Search code examples
javaspringspring-bootweb-servicesjax-ws

Spring Boot register JAX-WS webservice as bean


In my spring boot ws based application I have created a jax-ws webservice following a contract first approach. The Web service is up but I cannot autowire my other beans inside my webservice.

How can I define, my webservice in spring as bean?

Following is my webservice impl class:

@WebService(endpointInterface = "com.foo.bar.MyServicePortType")
@Service
public class MySoapService implements MyServicePortType {
    @Autowired
    private MyBean obj;

    public Res method(final Req request) {
        System.out.println("\n\n\nCALLING.......\n\n" + obj.toString()); //obj is null here
        return new Res();
    }
}

MyServicePortType is geneated by maven from wsdl file

When I call this service (via SoapUi) it gives NullPointerException as the MyBean object is not autowired.

Since my application is built on Spring boot, there is no xml file. Currently I have sun-jaxws.xml file with endpoint configuration. How can I do following configuration in spring boot application

<wss:binding url="/hello">
    <wss:service>
        <ws:service bean="#helloWs"/>
    </wss:service>
</wss:binding>

Following is my SpringBootServletInitializer class:

@Configuration
public class WebXml extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(final SpringApplicationBuilder application) {
        return application.sources(WSApplication.class);
    }

    @Bean
    public ServletRegistrationBean jaxws() {
        final ServletRegistrationBean jaxws = new ServletRegistrationBean(new WSServlet(), "/jaxws");
        return jaxws;
    }

    @Override
    public void onStartup(final ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        servletContext.addListener(new WSServletContextListener());
    }
}

Solution

  • Extending SpringBeanAutowiringSupport is the recommended way to get beans injected for JAX-WS endpoint class, from current spring root web application context. However this does not work with spring boot as it's a bit different on servlet context initialization.

    Problem

    SpringBootServletInitializer.startup() uses a custom ContextLoaderListener and does not pass the created application context to ContextLoader. Later when the object of JAX-WS endpoint class being initialized, SpringBeanAutowiringSupport depends on ContextLoader to retrieve the current application context, and always get null.

    public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
    
        @Override
        public void onStartup(ServletContext servletContext) throws ServletException {
            WebApplicationContext rootAppContext = createRootApplicationContext(
                    servletContext);
            if (rootAppContext != null) {
                servletContext.addListener(new ContextLoaderListener(rootAppContext) {
                    @Override
                    public void contextInitialized(ServletContextEvent event) {
                        // no-op because the application context is already initialized
                    }
                });
            }
            ......
        }
    }
    

    Workaround

    You could register a bean that implements org.springframework.boot.context.embedded.ServletContextInitializer to retrieve the application context during startup().

    @Configuration
    public class WebApplicationContextLocator implements ServletContextInitializer {
    
        private static WebApplicationContext webApplicationContext;
    
        public static WebApplicationContext getCurrentWebApplicationContext() {
            return webApplicationContext;
        }
    
        @Override
        public void onStartup(ServletContext servletContext) throws ServletException {
            webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        }
    }
    

    Then you could implement self-autowiring in your JAX-WS endpoint class.

    @WebService
    public class ServiceImpl implements ServicePortType {
    
        @Autowired
        private FooBean bean;
    
        public ServiceImpl() {
            AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
            WebApplicationContext currentContext = WebApplicationContextLocator.getCurrentWebApplicationContext();
            bpp.setBeanFactory(currentContext.getAutowireCapableBeanFactory());
            bpp.processInjection(this);
        }
    
        // alternative constructor to facilitate unit testing.
        protected ServiceImpl(ApplicationContext context) {
            AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
            bpp.setBeanFactory(new DefaultListableBeanFactory(context));
            bpp.processInjection(this);
        }
    }
    

    Unit Testing

    In unit tests you could get current spring application context injected, and call alternative constructor with it.

    @Autowired 
    private ApplicationContext context;
    
    private ServicePortType service;
    
    @Before
    public void setup() {
        this.service = new ServiceImpl(this.context);
    }