Search code examples
springspring-bootjax-rscxfcdi

Spring Boot with Apache CXF and CDI


When using Apache's CXF JAX-RS Spring Boot starter with the CXF CDI dependency (cxf-integration-cdi), Spring fails trying to do the autowiring because it only supports JSR 330 and not CDI. Is there a way to get CDI to work with Spring Boot?

Code:

package com.ibm.test.webservices;

import javax.enterprise.inject.Any;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Application;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@ApplicationPath("/")
@Path("/")
public class TestWebServices extends Application {
    public static void main(String[] args) {
        SpringApplication.run(
            TestWebServices.class,
            "--cxf.path=/",
            "--cxf.jaxrs.classes-scan=true",
            "--cxf.jaxrs.classes-scan-packages=" +
                TestWebServices.class.getPackage().getName()
        );
    }

    @Inject
    @Any
    private Instance<InvokerInterface> impl;

    @GET
    @Produces("text/plain")
    @Path("/")
    public String helloWorld() {
        return impl.get().invoke();
    }

    public interface InvokerInterface {
        String invoke();
    }

    @Named
    @Default
    public static class Implementation1 implements InvokerInterface {
        public String invoke() {
            return "Hello World 1\n";
        }
    }

    @Named
    public static class Implementation2 implements InvokerInterface {
        public String invoke() {
            return "Hello World 2\n";
        }
    }
}

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ibm.test</groupId>
    <artifactId>test-spring-boot-with-cdi</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Test Spring Boot with Apache CXF and CDI</name>

    <!-- We need to use cxf-spring-boot-starter-jaxrs 3.2.0 because of https://issues.apache.org/jira/browse/CXF-7237 
        At the time of writing this code, the latest available version in Maven central 
        is 3.1.7 so we need to use the Apache snapshot repository. -->
    <repositories>
        <repository>
            <id>apache.snapshots</id>
            <name>Apache Development Snapshot Repository</name>
            <url>https://repository.apache.org/content/repositories/snapshots/</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
            <version>3.2.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>javax.inject</groupId>
            <artifactId>javax.inject</artifactId>
            <version>1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-integration-cdi</artifactId>
            <version>3.1.11</version>
        </dependency>
    </dependencies>

    <!-- Required for a standalone JAR: -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Exception:

2017-07-28 16:32:59.527 ERROR 9630 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Field impl in com.ibm.test.webservices.TestWebServices required a bean of type 'javax.enterprise.inject.Instance' that could not be found.


Action:

Consider defining a bean of type 'javax.enterprise.inject.Instance' in your configuration.

[WARNING] 
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:90)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:55)
    at java.lang.reflect.Method.invoke(Method.java:508)
    at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:527)
    at java.lang.Thread.run(Thread.java:785)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'testWebServices': Unsatisfied dependency expressed through field 'impl'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.enterprise.inject.Instance<com.ibm.test.webservices.TestWebServices$InvokerInterface>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@javax.inject.Inject(), @javax.enterprise.inject.Any()}
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107)
    at com.ibm.test.webservices.TestWebServices.main(TestWebServices.java:22)
    ... 6 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.enterprise.inject.Instance<com.ibm.test.webservices.TestWebServices$InvokerInterface>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@javax.inject.Inject(), @javax.enterprise.inject.Any()}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1493)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1104)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:585)
    ... 25 more

Solution

  • The key insight is that Spring autowiring (e.g. scanBasePackages on @SpringBootApplication, @ComponentScan, etc.) must be avoided. The following worked:

    TestSpringBootApplication.java:

    package com.test.webservices;
    
    import org.apache.cxf.cdi.CXFCdiServlet;
    import org.jboss.weld.environment.se.Weld;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.web.servlet.ServletComponentScan;
    import org.springframework.boot.web.servlet.ServletRegistrationBean;
    import org.springframework.boot.web.support.SpringBootServletInitializer;
    import org.springframework.context.ApplicationContextInitializer;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.context.annotation.Bean;
    
    @SpringBootApplication
    // @ServletComponentScan only occurs with an embedded web server, and this is
    // needed for Tomcat:
    // https://docs.jboss.org/weld/reference/latest/en-US/html/environments.html#_tomcat
    @ServletComponentScan(basePackageClasses = { org.jboss.weld.environment.servlet.Listener.class })
    public class TestSpringBootApplication extends SpringBootServletInitializer {
    
        public static void main(String[] args) {
            SpringApplication app = new SpringApplication(TestSpringBootApplication.class);
            app.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
                @Override
                public void initialize(ConfigurableApplicationContext applicationContext) {
                    new Weld().initialize();
                }
            });
            app.run(args);
        }
    
        @Bean
        public ServletRegistrationBean cxfServletRegistration() {
            // http://cxf.apache.org/docs/using-cxf-and-cdi-11-jsr-346.html
            ServletRegistrationBean registration = new ServletRegistrationBean(new CXFCdiServlet(), "/*");
            registration.setLoadOnStartup(1);
            return registration;
        }
    }
    

    TestWebServicesApplication.java:

    package com.test.webservices;
    
    import javax.ws.rs.ApplicationPath;
    import javax.ws.rs.core.Application;
    
    @ApplicationPath("/")
    public class TestWebServicesApplication extends Application {
    }
    

    TestWebServices.java:

    package com.test.webservices;
    
    import javax.enterprise.inject.Any;
    import javax.enterprise.inject.Instance;
    import javax.enterprise.inject.Vetoed;
    import javax.inject.Inject;
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.Produces;
    
    @Path("/")
    public class TestWebServices {
        @Inject
        @Any
        private Instance<InvokerInterface> impl;
    
        @GET
        @Produces("text/plain")
        @Path("/")
        public String helloWorld() {
            return impl.get().invoke();
        }
    
        public interface InvokerInterface {
            String invoke();
        }
    
        public static class Implementation1 implements InvokerInterface {
            public String invoke() {
                return "Hello World 1\n";
            }
        }
    
        @Vetoed
        public static class Implementation2 implements InvokerInterface {
            public String invoke() {
                return "Hello World 2\n";
            }
        }
    }
    

    pom.xml:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.test</groupId>
        <artifactId>test-spring-boot-with-cdi</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>war</packaging>
    
        <name>Test Spring Boot with Apache CXF and CDI</name>
    
        <properties>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
    
        <!-- We need to use cxf-spring-boot-starter-jaxrs 3.2.0 because of https://issues.apache.org/jira/browse/CXF-7237 
            At the time of writing this code, the latest available version in Maven central 
            is 3.1.7 so we need to use the Apache snapshot repository. -->
        <repositories>
            <repository>
                <id>apache.snapshots</id>
                <name>Apache Development Snapshot Repository</name>
                <url>https://repository.apache.org/content/repositories/snapshots/</url>
                <releases>
                    <enabled>false</enabled>
                </releases>
                <snapshots>
                    <enabled>true</enabled>
                </snapshots>
            </repository>
        </repositories>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>1.5.6.RELEASE</version>
        </parent>
        <dependencies>
            <dependency>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
                <version>3.2.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>javax.inject</groupId>
                <artifactId>javax.inject</artifactId>
                <version>1</version>
            </dependency>
            <dependency>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-integration-cdi</artifactId>
                <version>3.1.11</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.weld.servlet</groupId>
                <artifactId>weld-servlet</artifactId>
                <version>2.4.4.Final</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.weld.se</groupId>
                <artifactId>weld-se</artifactId>
                <version>2.4.4.Final</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.jaxrs</groupId>
                <artifactId>jackson-jaxrs-json-provider</artifactId>
            </dependency>
            <dependency>
                <groupId>org.glassfish</groupId>
                <artifactId>javax.json</artifactId>
                <version>1.0.4</version>
            </dependency>
        </dependencies>
    
        <!-- Required for a standalone JAR: -->
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>
    

    src/main/resources/META-INF/beans.xml:

    <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
           version="1.1" bean-discovery-mode="all">
    </beans>
    

    src/main/resources/META-INF/context.xml:

    <!-- Required for Tomcat: https://docs.jboss.org/weld/reference/latest/en-US/html/environments.html#_tomcat -->
    <Context>
        <Resource name="BeanManager" auth="Container"
            type="javax.enterprise.inject.spi.BeanManager" factory="org.jboss.weld.resources.ManagerObjectFactory" />
    </Context>
    

    To use JAXB in addition to JSON, it seems that CXF's JAXRSCdiResourceExtension doesn't find any @Providers, so I also added a CDI Extension that creates JacksonJaxbJsonProvider as an AnnotatedType (this also seems to require that the JAX-RS Application object does not override getClasses or getSingletons and instead auto-discovers all @Paths):

    package com.test;
    
    import javax.enterprise.event.Observes;
    import javax.enterprise.inject.spi.AnnotatedType;
    import javax.enterprise.inject.spi.BeanManager;
    import javax.enterprise.inject.spi.BeforeBeanDiscovery;
    
    import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
    
    public class RegisterCDIBeans implements javax.enterprise.inject.spi.Extension {
        public void beforeBeanDiscovery(@Observes BeforeBeanDiscovery bbd, BeanManager beanManager) {
            final AnnotatedType<?> annotatedType = beanManager.createAnnotatedType(JacksonJaxbJsonProvider.class);
            bbd.addAnnotatedType(annotatedType, annotatedType.toString());
        }
    }
    

    And then register the CDI extension in src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension:

    com.test.RegisterCDIBeans