CDI Extensions: Can I expose one interface in two scopes?
I have an interface in a unit testing framework: CDIMocker. I'm currently using an interceptor to allow mocking in a CDI container. It's an experiment - one of a couple of approaches to unit testing I'm considering. (The other main contender is to use constructor and method injection for all beans and unit test outside CDI - in which case this work becomes more a learning exercise in CDI Extensions).
I have two custom scopes - TestClassScoped and TestMethodScoped. My JUnit4 custom runner wraps the appropriate Class and Method blocks in statements that start and stop these scopes as needed. It also starts an instance of Weld-SE if needed. It knows if it's in CDI because the Extension remembers.
The mocker interface is the same wherever it us used. It would be nice to expose it in both scopes, so I could
// Sadly Static Injection currently doesn't work, but if it did
@Inject @TestClassScoped
private static CdiMocker s_classScopedMocker
@Inject @TestMethodScoped
private CdiMocker m_methodScopedMocker
There are other obvious ways. I currently have a factory method on a singleton outside CDI that can return either of these instances (ThreadLocal), or create a new short lived one. I have also had success creating two concrete classes and declaring different scopes on them.
I've tried using Producer Methods annotated as above, but no luck. Perhaps a simple mistake, perhaps a misunderstanding.
@Produces @TestClassScoped
public CdiMocker getClassScopedMockerForCdi()
{
return getTestClassContext().getMocker();
}
@Produces @TestMethodScoped
public CdiMocker getMethodScopedMockerForCdi()
{
return getTestMethodContext().getMocker();
}
I thought from some part of the CDI documentation it was possible to declare scopes on injection points as I have done, but I note that the Instance<> interface does not allow me to select() using scoped annotation so maybe that is wrong.
I could provide two qualifiers. Can an annotation be a Qualifier and a Scope at the same time?
Another idea would be to have my extension provide two Bean<CdiMocker>, both exposing the same class but in different scopes. They could also provide custom create() and destroy() because the CdiMocker instances are managed by my two custom Contexts. The impression I get of CDI is that a given Class can only live in one Scope, so would this be Wrong?
Any suggestions on what is best?
Thanks - Richard
(I'd love to open source the result, but have done enough in work time I'd have to ask so not likely. The business argument would be public review. I use an Interceptor now with the disadvantage that it has to be left in place, but wonder if I could achieve something by intercepting the bean lifecycle in the extension. We can use Alternatives for things like the comms layer that talks to our legacy app server, but for some things a single unit test wants a custom mock and Alternatives are to开发者_StackOverflowo global.)
I've created
@Qualifier
@Target({TYPE, METHOD, PARAMETER, FIELD})
@Retention(RUNTIME)
@Documented
public @interface Scoped
{
Class<? extends Annotation> value();
}
I currently have two Bean implementations. Relevant (unusual) parts are:
/**
* A Bean<> implementation for the CdiMocker beans
*/
class MockerBean implements Bean<CdiMocker>
{
private final class ScopedAnnotation extends AnnotationLiteral<Scoped> implements Scoped
{
private static final long serialVersionUID = 1L;
public Class<? extends Annotation> value() { return m_context.getScope(); }
}
private final CdiMockContextControl m_context;
public MockerBean(CdiMockContextControl context)
{
m_context = context;
}
The bean class is CdiMocker.class
@Override
public Class<?> getBeanClass()
{
return CdiMocker.class;
}
The Qualifiers include my ScopedAnnotation defined above. I've also included Default and Any. Maybe I need to remove these?
The scope is returned by my CdiMockContextControl interface.
@Override
public Class<? extends Annotation> getScope()
{
return m_context.getScope();
}
Type is my CdiMocker interface
@Override
public Set<Type> getTypes()
{
Set<Type> types = new HashSet<Type>();
types.add(CdiMocker.class);
types.add(Object.class);
return types;
}
Because the lifecycle is managed elsewhere I return the existing one.
@Override
public CdiMocker create(CreationalContext<CdiMocker> arg0)
{
return m_context.getMocker();
}
... and don't destroy it.
@Override
public void destroy(CdiMocker instance, CreationalContext<CdiMocker> ctx)
{
// It is managed by the Context, so I must not destroy it here.
ctx.release();
}
The solution is using Qualifiers, so I suppose it is now "Correct". I assume I can use lifecycle management in this way?
My test class (which my Runner instantiates using CDI) has
/**
* My CDI Extension makes a Mocking Context available in the Test Method Scope.
* This will be created before every test method, then destroyed afterwards.
*/
@Inject @Scoped(TestMethodScoped.class)
private CdiMocker m_testMethodMocker;
- Richard
精彩评论