Integrate 3rd party library with CDI, part 2: avoid new annotations for interceptors
If we take the Shiro-CDI integration case, here what we want to do:
@ApplicationScoped
public class Service {
@RequiresRoles("rtest")
public void roleTest() {
}
@RequiresPermissions("ptest")
public void permTest() {
}
@RequiresGuest
public void guest() {
}
@RequiresUser
public void user() {
}
@RequiresAuthentication
public void authenticated() {
}
}
Concretely we want to just decorate our CDI beans with Shiro annotations without requiring any other annotation.
Implement the interceptor
Our backbone will - as often in such a case - will use an interceptor:
public class ShiroInterceptor implements Serializable {
@AroundInvoke
public Object around(final InvocationContext ic) throws Exception {
validate(ic);
try {
return ic.proceed();
} catch (final Throwable throwable) {
if (Exception.class.isInstance(throwable)) {
throw Exception.class.cast(throwable);
}
if (Error.class.isInstance(throwable)) {
throw Error.class.cast(throwable);
}
throw new IllegalStateException(throwable);
}
}
}
All the challenge is how to implement the validate() method.
In Shiro case you will see it is not hard since Shiro provides MethodInterceptors for all annotations. To bridge it to CDI we just need to just bridge the InvocationContext to a MethodInvocation which are almost the same:
public class MethodInvocationContext implements MethodInvocation {
private final InvocationContext ic;
public MethodInvocationContext(final InvocationContext ic) {
this.ic = ic;
}
@Override
public Object proceed() throws Throwable {
return ic.proceed();
}
@Override
public Method getMethod() {
return ic.getMethod();
}
@Override
public Object[] getArguments() {
return ic.getParameters();
}
@Override
public Object getThis() {
return ic.getTarget();
}
}
Nothing complicated for now. Now we need to just delegate the invocation to shiro handler by annotation.
Side note: we'll not do it in this post to not make it too long but using an extension you can go a bit further and support dynamic annotations (through AnnotatedMethod).
All Shiro annotations have their own handler so I'll just present @RequiresRoles one.
First step is to bridge CDI and Shiro API:
public abstract class ShiroInterceptorBridge implements Serializable {
private final MethodInterceptorSupport delegate;
protected ShiroInterceptorBridge(final MethodInterceptorSupport delegate) {
this.delegate = delegate;
}
@AroundInvoke
public Object around(final InvocationContext ic) throws Exception {
try {
return delegate.invoke(new MethodInvocationContext(ic));
} catch (final Throwable throwable) {
if (Exception.class.isInstance(throwable)) {
throw Exception.class.cast(throwable);
}
if (Error.class.isInstance(throwable)) {
throw Error.class.cast(throwable);
}
throw new IllegalStateException(throwable);
}
}
}
The implementation is very simple: we just fully delegate to Shiro wiring the InvocationContext to a MethodInvocation.
Then to create an actual interceptor we define an interceptor and set the right Shiro handler inheriting from previous abstract class:
@Interceptor
@RequiresRoles("")
@Priority(Interceptor.Priority.LIBRARY_BEFORE)
public class RequiresRolesInterceptor extends ShiroInterceptorBridge {
public RequiresRolesInterceptor() {
super(new RoleAnnotationMethodInterceptor());
}
}
Here are the important points there:
- @Interceptor: mark the class as interceptor
- @Priority: ensure our interceptor is automatically activated and executed very early (LIBRARY_BEFORE)
- @RequiresRole: a CDI interceptor needs an @InterceptorBinding and since we don't want to add another annotation we'll reuse the Shiro one
Make the 3rd party library annotation binding
An annotation is binding if it has @InterceptorBinding.
The other issue we'll have is CDI uses parameters to distinguish between bindings: the annotation matching includes parameters values. For us it means our interceptor will only match the parameter ""...which will likely never happen. Of course you can define as much interceptors as roles, permissions etc...but this is not doable in a generic integration.
Of course CDI provides a solution: if you don't use that feature and just use the annotation method as a configuration option you can make CDI ignore it in binding resolution with the annotation @Nonbinding.
So overall question is: how to request to CDI to use the Shiro annotation considering it has @InterceptorBinding on the class and @Nonbinding on all methods.
To do that we need a CDI Extension. This Extension will be responsible to add the annotation as @InterceptorBinding programmatically adding @Nonbinding on the model of the annotation.
Here is the basic idea of the extension (I used Java 8 Stream but an old for loop works very well too):
public class ShiroExtension implements Extension {
void makeShiroAnnotationsInterceptorBindings(@Observes final BeforeBeanDiscovery e, final BeanManager bm) {
Stream.of(
RequiresRoles.class,
RequiresPermissions.class,
RequiresAuthentication.class,
RequiresUser.class,
RequiresGuest.class)
.forEach(type -> {
AnnotatedType<?> at = bm.createAnnotatedType(type);
e.addInterceptorBinding(new NonBindingAnnotation<>(at));
});
}
}
This extension just does what was described previously for all Shiro interceptor annotations.
The trick is still hidden in the NonBindingAnnotation class. This is just an implementation of AnnotatedType delegating all method to the CDI instance (at) except the annotations ones where it adds @InterceptorBinding:
import javax.enterprise.inject.spi.AnnotatedConstructor;
import javax.enterprise.inject.spi.AnnotatedField;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.util.AnnotationLiteral;
import javax.interceptor.InterceptorBinding;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
public class NonBindingAnnotation<T extends Annotation> implements AnnotatedType<T> {
private static final AnnotationLiteral<InterceptorBinding> INTERCEPTOR_BINDING_ANNOTATION_LITERAL = new InterceptorBindingLiteral();
private final AnnotatedType<T> delegate;
private final Set<AnnotatedMethod<? super T>> methods;
private final Set<Annotation> annotations;
public NonBindingAnnotation(final AnnotatedType<T> annotatedType) {
this.delegate = annotatedType;
this.methods = delegate.getMethods().stream()
.map(m -> new NonBindingMethod<>((AnnotatedMethod<T>) m))
.collect(toSet());
this.annotations = new HashSet<>(delegate.getAnnotations().size() + 1);
this.annotations.addAll(delegate.getAnnotations());
this.annotations.add(INTERCEPTOR_BINDING_ANNOTATION_LITERAL);
}
@Override
public Class<T> getJavaClass() {
return delegate.getJavaClass();
}
@Override
public Set<AnnotatedConstructor<T>> getConstructors() {
return delegate.getConstructors();
}
@Override
public Set<AnnotatedMethod<? super T>> getMethods() {
return methods;
}
@Override
public Set<AnnotatedField<? super T>> getFields() {
return delegate.getFields();
}
@Override
public Type getBaseType() {
return delegate.getBaseType();
}
@Override
public Set<Type> getTypeClosure() {
return delegate.getTypeClosure();
}
@Override
public <T extends Annotation> T getAnnotation(final Class<T> annotationType) {
return annotationType == InterceptorBinding.class ? annotationType.cast(INTERCEPTOR_BINDING_ANNOTATION_LITERAL) : delegate.getAnnotation(annotationType);
}
@Override
public Set<Annotation> getAnnotations() {
return annotations;
}
@Override
public boolean isAnnotationPresent(final Class<? extends Annotation> annotationType) {
return annotationType == InterceptorBinding.class || delegate.isAnnotationPresent(annotationType);
}
public static class InterceptorBindingLiteral
extends AnnotationLiteral<InterceptorBinding>
implements InterceptorBinding {
}
}
So now all annotations are usable for interceptors. If the annotation has no method/parameter then we are done. But role and permissions can be configured on the annotation in Shiro so we still need to add @Nonbinding to the methods to ensure we bind to our interceptor which ignores the parameters.
To do that the previous AnnotatedType implementation uses NonBindingMethod which does exactly the same but at AnnotatedMethod level and with the annotation @Nonbinding:
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedParameter;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.util.AnnotationLiteral;
import javax.enterprise.util.Nonbinding;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class NonBindingMethod<T extends Annotation> implements AnnotatedMethod<T> {
private static final AnnotationLiteral<Nonbinding> NONBINDING_ANNOTATION_LITERAL = new NonbindingLiteral();
private final AnnotatedMethod<T> delegate;
private final Set<Annotation> annotations;
NonBindingMethod(final AnnotatedMethod<T> m) {
delegate = m;
annotations = new HashSet<>(m.getAnnotations().size() + 1);
this.annotations.addAll(delegate.getAnnotations());
this.annotations.add(NONBINDING_ANNOTATION_LITERAL);
}
@Override
public Method getJavaMember() {
return delegate.getJavaMember();
}
@Override
public List<AnnotatedParameter<T>> getParameters() {
return delegate.getParameters();
}
@Override
public boolean isStatic() {
return delegate.isStatic();
}
@Override
public AnnotatedType<T> getDeclaringType() {
return delegate.getDeclaringType();
}
@Override
public Type getBaseType() {
return delegate.getBaseType();
}
@Override
public Set<Type> getTypeClosure() {
return delegate.getTypeClosure();
}
@Override
public <T extends Annotation> T getAnnotation(final Class<T> annotationType) {
return annotationType == Nonbinding.class ? annotationType.cast(NONBINDING_ANNOTATION_LITERAL) : delegate.getAnnotation(annotationType);
}
@Override
public Set<Annotation> getAnnotations() {
return annotations;
}
@Override
public boolean isAnnotationPresent(final Class<? extends Annotation> annotationType) {
return Nonbinding.class == annotationType || delegate.isAnnotationPresent(annotationType);
}
public static class NonbindingLiteral
extends AnnotationLiteral<Nonbinding>
implements Nonbinding {
}
}
Here we are, we have our annotation correctly integrated with CDI, we have our interceptor consistent with Shiro API so we just need to use it!
Conclusion
Integrating a 3rd party interceptor based API with CDI without duplicating or introducing new API generally just means:
- registering the library API as interceptor bindings
- ensuring parameters of the bindings are ignored by CDI
- implement an interceptor integrating with the library
This can look like a lot of code but if you think the annotation wrappers and literals are in a library you (re)use then it is really just the matter of the extension and is almost just 3 lines plus the interceptor itself which is not that big (depends the library). So concretely this is not a lot of code you own but it ensures you use a consistent programming model with the stack you rely on, in CDI or not :).
From the same author:
In the same category: