eclipse-ee4j / jersey

Eclipse Jersey Project - Read our Wiki:
https://github.com/eclipse-ee4j/jersey/wiki
Other
691 stars 357 forks source link

SpringComponentProvider does not work well with context hierarchy #3885

Open radarsh opened 6 years ago

radarsh commented 6 years ago

SpringComponentProvider does not seem to honour application context hierarchies. Say we have a parent context with common filters and other Jersey components defined and then we have a child context where we have everything in the parent context made available (via ApplicationContext.getParent) plus some additional components.

When we try to start such an application (I'm using Spring Boot), we get the below errors:

The following warnings have been detected: WARNING: HK2 service reification failed for [com.foo.MyFilter] with an exception:
MultiException stack 1 of 2
java.lang.NoSuchMethodException: Could not find a suitable constructor in com.foo.MyFilter class.
    at org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer.getConstructor(JerseyClassAnalyzer.java:192)
    at org.jvnet.hk2.internal.Utilities.getConstructor(Utilities.java:180)
    at org.jvnet.hk2.internal.ClazzCreator.initialize(ClazzCreator.java:129)
    at org.jvnet.hk2.internal.ClazzCreator.initialize(ClazzCreator.java:180)
    at org.jvnet.hk2.internal.SystemDescriptor.internalReify(SystemDescriptor.java:740)
    at org.jvnet.hk2.internal.SystemDescriptor.reify(SystemDescriptor.java:694)
    at org.jvnet.hk2.internal.ServiceLocatorImpl.reifyDescriptor(ServiceLocatorImpl.java:464)
    at org.jvnet.hk2.internal.ServiceLocatorImpl.narrow(ServiceLocatorImpl.java:2310)
    at org.jvnet.hk2.internal.ServiceLocatorImpl.access$1200(ServiceLocatorImpl.java:128)
    at org.jvnet.hk2.internal.ServiceLocatorImpl$9.compute(ServiceLocatorImpl.java:1395)
    at org.jvnet.hk2.internal.ServiceLocatorImpl$9.compute(ServiceLocatorImpl.java:1390)
    at org.glassfish.hk2.utilities.cache.internal.WeakCARCacheImpl.compute(WeakCARCacheImpl.java:128)
    at org.jvnet.hk2.internal.ServiceLocatorImpl.internalGetAllServiceHandles(ServiceLocatorImpl.java:1452)
    at org.jvnet.hk2.internal.ServiceLocatorImpl.getAllServiceHandles(ServiceLocatorImpl.java:1377)
    at org.jvnet.hk2.internal.ServiceLocatorImpl.getAllServiceHandles(ServiceLocatorImpl.java:1366)

The definition of MyFilter is a rather straightforward one:

@Component
public class MyFilter implements ContainerResponseFilter {

    private MyService myService;

    public MyFilter(MyService myService) {
        this.myService = myService;
    }

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        myService.doSomething(requestContext);
    }
}

The same filter works absolutely fine when loaded from the parent context. It's as though the child context doesn't seem to be able to see that this bean has already been defined in the parent context and therefore the Spring-HK2 bridge gives up and tries to do DI using HK2 instead of Spring.

Upon further investigation the root cause seems to be the below block of code in SpringComponentProvider.

@Override
public boolean bind(Class<?> component, Set<Class<?>> providerContracts) {

    if (ctx == null) {
        return false;
    }

    if (AnnotationUtils.findAnnotation(component, Component.class) != null) {
        String[] beanNames = ctx.getBeanNamesForType(component);
        if (beanNames == null || beanNames.length != 1) {
            LOGGER.severe(LocalizationMessages.NONE_OR_MULTIPLE_BEANS_AVAILABLE(component));
            return false;
        }

The problem lies in the line String[] beanNames = ctx.getBeanNamesForType(component);. ApplicationContext.getBeanNamesForType clearly states that it doesn't trace back the hierarchy:

Does not consider any hierarchy this factory may participate in. Use BeanFactoryUtils' beanNamesForTypeIncludingAncestors to include beans in ancestor factories too.

So in this case, SpringComponentProvider just expects to find MyFilter to be registered in the child context which obviously it isn't and just falls back to HK2 which isn't aware of the constructor injection already set up in Spring.

Let me know if you're accepting PRs and I'd be happy to submit one with the below change:

@Override
public boolean bind(Class<?> component, Set<Class<?>> providerContracts) {

    if (ctx == null) {
        return false;
    }

    if (AnnotationUtils.findAnnotation(component, Component.class) != null) {
-        String[] beanNames = ctx.getBeanNamesForType(component);
+        String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(ctx, component); 
        if (beanNames == null || beanNames.length != 1) {
            LOGGER.severe(LocalizationMessages.NONE_OR_MULTIPLE_BEANS_AVAILABLE(component));
            return false;
        }
jansupol commented 5 years ago

Yes, please, provide a PR. Thank you.

radarsh commented 5 years ago

PR #4080 provided. Contribution guidelines could be improved to link to the project's wiki for developer resources such as CI, signing off commits and a lot more. I had to trawl through existing pull requests to find the required information.