You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Makes a few projects I'm integrating crest with CDI and goals of this issue are: 1. at least share how to do it, 2. show some limitation of crest currently (the biggest being the env threadlocal but I'm not sure how we would like to resolve it without breaking).
Integration
Personally I extend crest with 4 "points":
a CDI aware main to scan with CDI the commands
a CDI extension to make @Command a qualifier and optionally adds it on classes using @Command at method only level. All annotation methods will be set as @NonBinding
a CDI aware environment for service injection (command parameters)
a CDI target indeed to use the right bean instance
CDI Main
public class CdiCrestMain extends Main {
private final BeanManager beanManager;
private final DefaultsContext context;
public CdiCrestMain(final BeanManager beanManager, final DefaultsContext context) {
super(context);
this.beanManager = beanManager;
this.context = context;
registerCommands();
}
public Map<String, Cmd> commands() {
return commands;
}
private void registerCommands() {
final var qualifier = new CommandQualifier();
this.beanManager.getBeans(Object.class, qualifier)
.forEach(bean -> commands.putAll(Commands.get(bean.getBeanClass(), new CdiTarget(beanManager, bean), context)));
}
public void apply(final String... args) throws Exception {
final var environment = new CDIEnvironment(beanManager);
final var envHolder = Environment.ENVIRONMENT_THREAD_LOCAL;
envHolder.set(environment);
try {
super.main(environment, args);
} finally {
// crest API has a default value so we can't really store previous value and reset it
envHolder.remove();
}
}
private static class CommandQualifier extends AnnotationLiteral<Command> implements Command {
@Override
public String value() {
return null;
}
@Override
public String usage() {
return null;
}
@Override
public Class<?>[] interceptedBy() {
return new Class[0];
}
}
}
Here see the limitatio nof the threadlocal in apply, we should be able to stack it to reset it after usage but here we can't since it has a default. An option is Environment.hasValue() which would avoid the default and Environment.value() but it would break backward compatibility so not sure the goal. I would also prefer the environment to be passed to the commands like the context and not depend on a thread local.
CDI Environment
public class CDIEnvironment extends SystemEnvironment {
private final BeanManager beanManager;
public CDIEnvironment(final BeanManager beanManager) {
this.beanManager = beanManager;
}
@Override
public <T> T findService(final Class<T> type) {
return ofNullable(super.findService(type))
.orElseGet(() -> {
final var bean = beanManager.resolve(beanManager.getBeans(type));
if (bean == null || !beanManager.isNormalScope(bean.getScope())) {
throw new IllegalStateException("For now only normal scoped beans can be used as command parameter injection.");
}
return type.cast(beanManager.getReference(bean, type, beanManager.createCreationalContext(null)));
});
}
}
We can make it supporting not normal scoped beans - not sure it is that useful, maybe, but it means storing the instances for the call duration and release the context after. here the API should be enhanced to return a CrestInstance() { T value(); void close(); } which would be automatically stored/unwrapped by the runtime.
CDI Target
public class CdiTarget implements Target {
private final BeanManager beanManager;
private final Bean<?> bean;
public CdiTarget(final BeanManager beanManager, final Bean<?> bean) {
this.beanManager = beanManager;
this.bean = bean;
}
@Override
public Object invoke(final Method method, final Object... args) throws InvocationTargetException, IllegalAccessException {
return method.invoke(getInstance(method), args);
}
@Override
public Object getInstance(final Method method) {
final var creationalContext = beanManager.createCreationalContext(null);
return beanManager.getReference(bean, bean.getBeanClass(), creationalContext);
}
}
Here again we have the same limitation than for the services, CrestInstance can solve it. Theorically we should be able to have a more adapted API since getInstance is used for bean validation whereas we should get something like <T, Object> T withInstance(Function<T, Object>) API which would wrap the bean validation+method invocation to lookup a single time the instance and be able to release it after method invocation (which would need the instance to call injected in parameters for ex).
Here again, not sure in terms of backward compatibility what is desired.
CDI Extension to make @Command a qualifier
This one is not 100% required and can be replaced by an extension for the scanning but this implementation is faster (always better for a CLI) even if it has the limitation to not enable to have alternatives for commands - which is not a big limitation since it can be replaced by a small indirection if needed (never?).
public class CrestCommandQualifierExtension implements Extension {
public void onStart(@Observes final BeforeBeanDiscovery beforeBeanDiscovery,
final BeanManager beanManager) {
beforeBeanDiscovery.addQualifier(new NonBindingType<>(beanManager.createAnnotatedType(Command.class)));
}
private static class NonBindingType<T> implements AnnotatedType<T> {
private final AnnotatedType<T> type;
private final Set<AnnotatedMethod<? super T>> methods;
private NonBindingType(final AnnotatedType<T> annotatedType) {
this.type = annotatedType;
this.methods = annotatedType.getMethods().stream()
.map(NonBindingMethod::new)
.collect(toSet());
}
@Override
public Class<T> getJavaClass() {
return type.getJavaClass();
}
@Override
public Set<AnnotatedConstructor<T>> getConstructors() {
return type.getConstructors();
}
@Override
public Set<AnnotatedMethod<? super T>> getMethods() {
return methods;
}
@Override
public Set<AnnotatedField<? super T>> getFields() {
return type.getFields();
}
@Override
public <T1 extends Annotation> Set<T1> getAnnotations(final Class<T1> annotationType) {
return type.getAnnotations(annotationType);
}
@Override
public Type getBaseType() {
return type.getBaseType();
}
@Override
public Set<Type> getTypeClosure() {
return type.getTypeClosure();
}
@Override
public <X extends Annotation> X getAnnotation(final Class<X> aClass) {
return type.getAnnotation(aClass);
}
@Override
public Set<Annotation> getAnnotations() {
return type.getAnnotations();
}
@Override
public boolean isAnnotationPresent(final Class<? extends Annotation> aClass) {
return type.isAnnotationPresent(aClass);
}
}
private static class NonBindingMethod<A> implements AnnotatedMethod<A> {
private final AnnotatedMethod<A> delegate;
private final Set<Annotation> annotations;
private NonBindingMethod(final AnnotatedMethod<A> delegate) {
this.delegate = delegate;
this.annotations = Stream.concat(
delegate.getAnnotations().stream(),
Stream.of(Nonbinding.Literal.INSTANCE))
.collect(toSet());
}
@Override
public Method getJavaMember() {
return delegate.getJavaMember();
}
@Override
public <T extends Annotation> Set<T> getAnnotations(final Class<T> annotationType) {
return delegate.getAnnotations(annotationType);
}
@Override
public List<AnnotatedParameter<A>> getParameters() {
return delegate.getParameters();
}
@Override
public boolean isStatic() {
return delegate.isStatic();
}
@Override
public AnnotatedType<A> 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> aClass) {
return aClass == Nonbinding.class ? aClass.cast(Nonbinding.Literal.INSTANCE) : delegate.getAnnotation(aClass);
}
@Override
public Set<Annotation> getAnnotations() {
return annotations;
}
@Override
public boolean isAnnotationPresent(final Class<? extends Annotation> aClass) {
return Nonbinding.class == aClass || delegate.isAnnotationPresent(aClass);
}
}
}
To support method only level commands you can also add:
public <T> void markAtClassLevelMethodOnlyCommands(@Observes final ProcessAnnotatedType<T> pat) {
final var annotatedType = pat.getAnnotatedType();
if (!annotatedType.isAnnotationPresent(Command.class) &&
annotatedType.getMethods().stream().anyMatch(m -> m.isAnnotationPresent(Command.class))) {
pat.configureAnnotatedType().add(new AnnotationLiteral<Command>() { // todo: extract CommandQualifier from main to make it a constant if this impl is desired
});
}
}
I tend to avoid to use this since it makes the extenson o(n) instead of o(1).
I also use the extension without the SPI registration using CDI SE API:
public static void main(final String... args) throws Exception {
try (final var container = SeContainerInitializer.newInstance()
.addExtensions(new CrestCommandQualifierExtension())
.initialize()) {
new CdiCrestMain(container.getBeanManager(), new SystemPropertiesDefaultsContext()).apply(args);
}
}
The text was updated successfully, but these errors were encountered:
Hi,
Makes a few projects I'm integrating crest with CDI and goals of this issue are: 1. at least share how to do it, 2. show some limitation of crest currently (the biggest being the env threadlocal but I'm not sure how we would like to resolve it without breaking).
Integration
Personally I extend crest with 4 "points":
@Command
a qualifier and optionally adds it on classes using@Command
at method only level. All annotation methods will be set as@NonBinding
CDI Main
Here see the limitatio nof the threadlocal in
apply
, we should be able to stack it to reset it after usage but here we can't since it has a default. An option isEnvironment.hasValue()
which would avoid the default andEnvironment.value()
but it would break backward compatibility so not sure the goal. I would also prefer the environment to be passed to the commands like the context and not depend on a thread local.CDI Environment
We can make it supporting not normal scoped beans - not sure it is that useful, maybe, but it means storing the instances for the call duration and release the context after. here the API should be enhanced to return a
CrestInstance() { T value(); void close(); }
which would be automatically stored/unwrapped by the runtime.CDI Target
Here again we have the same limitation than for the services,
CrestInstance
can solve it. Theorically we should be able to have a more adapted API sincegetInstance
is used for bean validation whereas we should get something like<T, Object> T withInstance(Function<T, Object>)
API which would wrap the bean validation+method invocation to lookup a single time the instance and be able to release it after method invocation (which would need the instance to call injected in parameters for ex).Here again, not sure in terms of backward compatibility what is desired.
CDI Extension to make
@Command
a qualifierThis one is not 100% required and can be replaced by an extension for the scanning but this implementation is faster (always better for a CLI) even if it has the limitation to not enable to have alternatives for commands - which is not a big limitation since it can be replaced by a small indirection if needed (never?).
To support method only level commands you can also add:
I tend to avoid to use this since it makes the extenson
o(n)
instead ofo(1)
.I also use the extension without the SPI registration using CDI SE API:
The text was updated successfully, but these errors were encountered: