13

In a spring based project I am working on, there's a layer of functionality for calling web service. For each web service operation, a method is created with almost same code but with some different, operation specific, information(e.g. service name, operation name, namespaces, etc).

I am replacing this layer with interfaces and annotated methods. For example, below code is provided for operation "fetchBar" of web service("foo").

package a.b.c.webservices;

@WebService(service="foo", namespace="...")
public interface FooWebService {

    @WebServiceOperation(operation="fetchBar")
    BarRespons fetchBar(BarRequest request) throws WebServiceException;
}

Now I want, with some mechanism, spring allow me to create dynamic proxy beans from some specified package(s) and I can use following code to call web service.

package a.b.c.business;

import a.b.c.webservices.FooWebService;

public class FooBusiness {

    @Autowired 
    FooWebService fooWebService;


    public Bar getBar() {

        Bar bar = null;            

        BarRequest request; 

        //create request
        BarResponse response = fooWebService.fetchBar(request);
        //extrac bar from response

        return bar;
    }
}

To achieve this I have created dynamic beans instances using java.lang.reflect.Proxy.newProxyInstance by providing it implementation of InvocationHandler. But Autowiring doesn't work in provided implementation of invocationHandler and in its further dependencies.

I tried following ways to achieve this.

  • Implemented BeanFactoryPostProcessor.postProcessBeanFactory and registered beans using ConfigurableListableBeanFactory.registerSingleton method.
  • Implemented ImportBeanDefinitionRegistrar.registerBeanDefinitions and tried to use BeanDefinitionRegistry.registerBeanDefinition but I am confused how to provide correct Bean definition that supports Autowiring.

Can any one tell me what is missing? Please guide me if I am not going in right direction.

Bilal Mirza
  • 2,576
  • 4
  • 30
  • 57

3 Answers3

18

Here's how I implemented all the functionality that creates beans of 'WebService' annotated interfaces and also supports Autowiring inside proxy implementation. (package declaration and import statements are omitted in below code) First of all I created WebService and WebServiceOperation annotation.

WebService Annotation

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebService {
    String service();
    String namespace();
}

WebService Operation Annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebServiceOperation {
    String operation();
}

Next step is to scan all WebService annotated interfaces from specified packages. Spring provides ClassPathScanningCandidateComponentProvider for package scanning but it does not detect interfaces. Please see this question and it's answer for more details. So I extended ClassPathScanningCandidateComponentProvider and overrode isCandidateComponent method.

ClassPathScanner

public class ClassPathScanner extends ClassPathScanningCandidateComponentProvider {

    public ClassPathScanner(final boolean useDefaultFilters) {
        super(useDefaultFilters);
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isIndependent();
    }

}

At this point I created EnableWebServices annotation to enable web services and to provide web service packages that contain WebService annotated interfaces.

EnableWebServices Annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({
    WebServiceProxyConfig.class, 
    WebServiceProxyBeansRegistrar.class
})

public @interface EnableWebServices {

    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

}

This annotation can be applied to some Configuration annotated class with packages to scan interfaces, as below.

@EnableWebServices({
    "a.b.c.webservices",
    "x.y.z.webservices"
})

It's time to think about dynamic proxy creation that will invoke actual web service from information given in WebService and WebServiceOperation annotations. Java provides a mechanism to create dynamic proxy which requires to provide implementation of InvocationHandler interface and provide logic in its invoke method. I named this implementaiton as WebServiceProxy

Suppose a bean of type 'TheWebServiceCaller' contains all nasty logic to call a web service. I just have inject it and to invoke it's call method with a TheWebServiceInfo (extracted from WebService and WebServiceOperation annotations) and request object.

TheWebServiceInfo(Suppose all fields have getters and setters)

public class TheWebServiceInfo {
    private String service;
    private String namespace;
    private String operation;
}

WebServiceProxy

public class WebServiceProxy implements InvocationHandler {

    @Autowired
    private TheWebServiceCaller caller;

    @Override
    public Object invoke(Object target, Method method, Object[] args) throws Exception {

        Object request = (null != args && args.length > 0) ? args[0] : null;

        WebService webService = method.getDeclaringClass().getAnnotation(WebService.class);
        WebServiceOperation webServiceOperation = method.getAnnotation(WebServiceOperation.class);

        TheWebServiceInfo theInfo = createTheWebServiceInfo(webService, webServiceOperation);

        return caller.call(theInfo, request);
    }

    private TheWebServiceInfo createTheWebServiceInfo(WebService webService, WebServiceOperation webServiceOperation) {
        TheWebServiceInfo theInfo = new TheWebServiceInfo();
        theInfo.setService(webService.service());
        theInfo.setNamespace(webService.namespace());
        theInfo.setOperation(webServiceOperation.operation());
        return theInfo;
    }
}

Implementaion of InvocationHandler is passed to Proxy.newProxyInstance (along with some other information) to create proxy objects. I need separat proxy objectes for each WebService annotated interface. I will now create a factory to proxy instances creation and name is as 'WebServiceProxyBeanFactory'. Instances created by this factory will become beans for corresponding WebService annotated interfaces.

A bit later, I will expose 'WebServiceProxy' and WebServiceProxyBeanFactory as beans. In 'WebServiceProxyBeanFactory', I will inject WebServiceProxy and used it. Please note that createWebServiceProxyBean uses generics. This is important.

WebServiceProxyBeanFactory

public class WebServiceProxyBeanFactory {

    @Autowired 
    WebServiceProxy webServiceProxy;

    @SuppressWarnings("unchecked")
    public <WS> WS createWebServiceProxyBean(ClassLoader classLoader, Class<WS> clazz) {
        return (WS) Proxy.newProxyInstance(classLoader, new Class[] {clazz}, webServiceProxy);
    }

}

If you remember, earlier I have imported WebServiceProxyConfig in EnableWebServices annotations. WebServiceProxyConfig is used to expose WebServiceProxy and WebServiceProxyBeanFactory as beans.

WebServiceProxyConfig

@Configuration
public class WebServiceProxyConfig {

    @Bean
    public WebServiceProxy webServiceProxy() {
        return new WebServiceProxy();
    }

    @Bean(name = "webServiceProxyBeanFactory")
    public WebServiceProxyBeanFactory webServiceProxyBeanFactory() {
        return new WebServiceProxyBeanFactory();
    }

}

Now everything is in place. it's time to write a hook to start scanning Web service packages and register dynamic proxies as beans. I will provide implementation of ImportBeanDefinitionRegistrar.

WebServiceProxyBeansRegistrar

@Configuration
public class WebServiceProxyBeansRegistrar implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware {

    private ClassPathScanner classpathScanner;
    private ClassLoader classLoader;

    public WebServiceProxyBeansRegistrar() {
        classpathScanner = new ClassPathScanner(false);
        classpathScanner.addIncludeFilter(new AnnotationTypeFilter(WebService.class));
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        String[] basePackages = getBasePackages(importingClassMetadata);
        if (ArrayUtils.isNotEmpty(basePackages)) {
            for (String basePackage : basePackages) {
                createWebServicProxies(basePackage, registry);
            }
        }
    }

    private String[] getBasePackages(AnnotationMetadata importingClassMetadata) {

        String[] basePackages = null;

        MultiValueMap<String, Object> allAnnotationAttributes = 
            importingClassMetadata.getAllAnnotationAttributes(EnableWebServices.class.getName());

        if (MapUtils.isNotEmpty(allAnnotationAttributes)) {
            basePackages = (String[]) allAnnotationAttributes.getFirst("basePackages");
        }

        return basePackages;
    }

    private void createWebServicProxies(String basePackage, BeanDefinitionRegistry registry) {
        try {

            for (BeanDefinition beanDefinition : classpathScanner.findCandidateComponents(basePackage)) {

                Class<?> clazz = Class.forName(beanDefinition.getBeanClassName());

                WebService webService = clazz.getAnnotation(WebService.class);

                String beanName = StringUtils.isNotEmpty(webService.bean())
                    ? webService.bean() : ClassUtils.getShortNameAsProperty(clazz);

                GenericBeanDefinition proxyBeanDefinition = new GenericBeanDefinition();
                proxyBeanDefinition.setBeanClass(clazz);

                ConstructorArgumentValues args = new ConstructorArgumentValues();

                args.addGenericArgumentValue(classLoader);
                args.addGenericArgumentValue(clazz);
                proxyBeanDefinition.setConstructorArgumentValues(args);

                proxyBeanDefinition.setFactoryBeanName("webServiceProxyBeanFactory");
                proxyBeanDefinition.setFactoryMethodName("createWebServiceProxyBean");

                registry.registerBeanDefinition(beanName, proxyBeanDefinition);

            }
        } catch (Exception e) {
            System.out.println("Exception while createing proxy");
            e.printStackTrace();
        }

    }

}

In this class, I extracted all packages provided in EnableWebServices annotation. for each extracted package, I used ClassPathScanner to scan. (Here logic can be refined to filter only WebService annotated interfaces). For each detected interface, I have registered a bean definitions. Please note I have used webServiceProxyBeanFactory and called its createWebServiceProxyBean with classLoader and type of interface. This factory method, when invoked by spring later, will return bean of same type as that of interface, so bean with correct type is registered. This bean can be injected anywhere with interface type. Moreover, WebServiceProxy can inject and use any other bean. So autowiring will also work as expected.

Community
  • 1
  • 1
Bilal Mirza
  • 2,576
  • 4
  • 30
  • 57
  • 1
    This worked perfect for me. I added a few lines into `getBasePackages()` method in the `registrar` to be sure of that if `basePackages` attribute is empty in `EnableWebServices` annotation, the registrar uses the package name of the class which is annotated with the enabler annotation: This will get that package name of the annotated class `String importingClassPackageName = ClassUtils.getPackageName(importingClassMetadata.getClassName());` – er-han Oct 26 '18 at 15:02
  • Thank you! I'm implementing a similar system which also involves dynamic proxy and injection, just like your example, and it's also like Spring Data JPA. This answer is very helpful. – ProtossShuttle Dec 28 '18 at 12:43
  • 1
    You may extend "AbstractInvocationHandler" instead implementing "InvocationHandler" otherwise "toString, hashCode and equals" will be handled by your invoke method which lead to unexpected behavior – dagi12 Jan 07 '20 at 10:46
  • Thanks @dagi12, I didn't know about `com.google.common.reflect.AbstractInvocationHandler` so I handled `hashCode`, `equals`, and `toString` in `WebServiceProxy` (This is not reflected in above solution). – Bilal Mirza Jan 07 '20 at 13:11
  • 1
    For anyone who is also about to go crazy like me. in your `ProxyBeanFactory.createProxyBean()`, you should use `java.lang.reflect.Proxy`, not the one from CGLib. Therefore, your Proxy should implement `java.lang.reflect.InvocationHandler`, again, not the one from cglib. therefore, you can use `com.google.common.reflect.AbstractInvocationHandler` to handle `hashcode, `equals` and `toString`. – Sepehr GH May 06 '20 at 11:50
1

Is your InvocationHandler a bean? You should create it as a bean, not just a simple object to get Autowired working

Yuri Plevako
  • 311
  • 1
  • 6
  • I exposed 'WebServiceProxy' (implements `InvocationHandler`) as bean and tried it in `BeanFactoryPostProcessor.postProcessBeanFactory()`. Here `beanFactory.getBean(WebServiceProxy.class)` returns instance of `WebServiceProxy` but it's 'Autowired' annotated fields are null. I need to get 'WebServiceProxy' instance here to pass it to `Proxy.newProxyInstance()`. – Bilal Mirza Sep 16 '16 at 04:17
  • 1
    Of course there are no autowired fields while BFPP is processing beanFactory. Fields are autowired at the stage of BeanPostProcessor.postProcessBeforeInitialization and it is fired after BFPP. – Yuri Plevako Sep 16 '16 at 07:14
  • You don't need to create any proxies at the stage of BFPP. If you really think you want to create proxies around some beans at the initialization stage, you need to create your BeanPostProcessor, not BeanFactoryPostProcessor, and create proxies in postProcessAfterInitialization – Yuri Plevako Sep 16 '16 at 07:16
  • I don't want to create proxies around some existing beans. I want to create beans for 'WebService' annotated interfaces. – Bilal Mirza Sep 16 '16 at 09:14
  • To create these beans you need to have you WebServices as beans, so Spring would know about them, and you wanna create proxies around them with BeanPostProcessor. What you wanna do is to create a proxy in postProcessAfterInitialization and return your proxy instead of the old bean. – Yuri Plevako Sep 16 '16 at 09:24
  • As `BeanPostProcessor` requires existing beans to create proxies around them, this will not fulfill my needs as I need to provide Interfaces and Interfaces are not detected as beans. I resolved the problem by implementing`ImportBeanDefinitionRegistrar.registerBeanDefinitions`, and defined 'InvocationHandler' implentation as bean (thank you for this idea). I will post my detailed answer some time later. – Bilal Mirza Sep 16 '16 at 11:42
1

I was thinking about the same problem but in a slightly more lightweight context. I don't need to load dynamicaly all the webservice clients. So instead I used a FactoryBean and within this factory bean I constructed the dynamic proxy. Here is one example where Autowiring of the service works:

public class CurrencyServiceWithDynamicProxy extends AbstractFactoryBean<CurrencyService> {

    ServiceClientConfiguration clientConfiguration;

    Object proxy;

    @Autowired
    public CurrencySyncFactoryDynamicProxy(ServiceClientConfigurationProvider serviceClientConfigurationProvider) {
        this.clientConfiguration = serviceClientConfigurationProvider.createClientConfig("currency");
        proxy = Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] { getObjectType() }, new MyInvocationHandler());
    }

    @Override
    public Class<CurrencySync> getObjectType() {
        // TODO Auto-generated method stub
        return CurrencyService.class;
    }

    @Override
    public CurrencySync createInstance() throws Exception {
          // do some creational logic
         return (CurrencySync)proxy;
    }

    public CurrencySync createService() {
        JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
        factory.setServiceClass(getObjectType());
        factory.getFeatures().add(som features);

        return getObjectType().cast(factory.create());
    }   
}

With respect of the accepted answer this factory example can easily be extended into a more dynamic version.

Alexander Petrov
  • 9,204
  • 31
  • 70