Java – Spring Boot register JAX-WS webservice as bean

javajax-wsspringspring-bootweb-services

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());
    }
}

Best 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);
}
Related Question