001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.beans.factory.config;
018
019import java.lang.reflect.Constructor;
020import java.lang.reflect.InvocationHandler;
021import java.lang.reflect.Method;
022import java.lang.reflect.Proxy;
023import java.util.Properties;
024
025import org.springframework.beans.BeanUtils;
026import org.springframework.beans.BeansException;
027import org.springframework.beans.FatalBeanException;
028import org.springframework.beans.factory.BeanFactory;
029import org.springframework.beans.factory.BeanFactoryAware;
030import org.springframework.beans.factory.FactoryBean;
031import org.springframework.beans.factory.InitializingBean;
032import org.springframework.beans.factory.ListableBeanFactory;
033import org.springframework.util.ReflectionUtils;
034import org.springframework.util.StringUtils;
035
036/**
037 * A {@link FactoryBean} implementation that takes an interface which must have one or more
038 * methods with the signatures {@code MyType xxx()} or {@code MyType xxx(MyIdType id)}
039 * (typically, {@code MyService getService()} or {@code MyService getService(String id)})
040 * and creates a dynamic proxy which implements that interface, delegating to an
041 * underlying {@link org.springframework.beans.factory.BeanFactory}.
042 *
043 * <p>Such service locators permit the decoupling of calling code from
044 * the {@link org.springframework.beans.factory.BeanFactory} API, by using an
045 * appropriate custom locator interface. They will typically be used for
046 * <b>prototype beans</b>, i.e. for factory methods that are supposed to
047 * return a new instance for each call. The client receives a reference to the
048 * service locator via setter or constructor injection, to be able to invoke
049 * the locator's factory methods on demand. <b>For singleton beans, direct
050 * setter or constructor injection of the target bean is preferable.</b>
051 *
052 * <p>On invocation of the no-arg factory method, or the single-arg factory
053 * method with a String id of {@code null} or empty String, if exactly
054 * <b>one</b> bean in the factory matches the return type of the factory
055 * method, that bean is returned, otherwise a
056 * {@link org.springframework.beans.factory.NoSuchBeanDefinitionException}
057 * is thrown.
058 *
059 * <p>On invocation of the single-arg factory method with a non-null (and
060 * non-empty) argument, the proxy returns the result of a
061 * {@link org.springframework.beans.factory.BeanFactory#getBean(String)} call,
062 * using a stringified version of the passed-in id as bean name.
063 *
064 * <p>A factory method argument will usually be a String, but can also be an
065 * int or a custom enumeration type, for example, stringified via
066 * {@code toString}. The resulting String can be used as bean name as-is,
067 * provided that corresponding beans are defined in the bean factory.
068 * Alternatively, {@linkplain #setServiceMappings(java.util.Properties) a custom
069 * mapping} between service IDs and bean names can be defined.
070 *
071 * <p>By way of an example, consider the following service locator interface.
072 * Note that this interface is not dependent on any Spring APIs.
073 *
074 * <pre class="code">package a.b.c;
075 *
076 *public interface ServiceFactory {
077 *
078 *    public MyService getService();
079 *}</pre>
080 *
081 * <p>A sample config in an XML-based
082 * {@link org.springframework.beans.factory.BeanFactory} might look as follows:
083 *
084 * <pre class="code">&lt;beans>
085 *
086 *   &lt;!-- Prototype bean since we have state -->
087 *   &lt;bean id="myService" class="a.b.c.MyService" singleton="false"/>
088 *
089 *   &lt;!-- will lookup the above 'myService' bean by *TYPE* -->
090 *   &lt;bean id="myServiceFactory"
091 *            class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
092 *     &lt;property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
093 *   &lt;/bean>
094 *
095 *   &lt;bean id="clientBean" class="a.b.c.MyClientBean">
096 *     &lt;property name="myServiceFactory" ref="myServiceFactory"/>
097 *   &lt;/bean>
098 *
099 *&lt;/beans></pre>
100 *
101 * <p>The attendant {@code MyClientBean} class implementation might then
102 * look something like this:
103 *
104 * <pre class="code">package a.b.c;
105 *
106 *public class MyClientBean {
107 *
108 *    private ServiceFactory myServiceFactory;
109 *
110 *    // actual implementation provided by the Spring container
111 *    public void setServiceFactory(ServiceFactory myServiceFactory) {
112 *        this.myServiceFactory = myServiceFactory;
113 *    }
114 *
115 *    public void someBusinessMethod() {
116 *        // get a 'fresh', brand new MyService instance
117 *        MyService service = this.myServiceFactory.getService();
118 *        // use the service object to effect the business logic...
119 *    }
120 *}</pre>
121 *
122 * <p>By way of an example that looks up a bean <b>by name</b>, consider
123 * the following service locator interface. Again, note that this
124 * interface is not dependent on any Spring APIs.
125 *
126 * <pre class="code">package a.b.c;
127 *
128 *public interface ServiceFactory {
129 *
130 *    public MyService getService (String serviceName);
131 *}</pre>
132 *
133 * <p>A sample config in an XML-based
134 * {@link org.springframework.beans.factory.BeanFactory} might look as follows:
135 *
136 * <pre class="code">&lt;beans>
137 *
138 *   &lt;!-- Prototype beans since we have state (both extend MyService) -->
139 *   &lt;bean id="specialService" class="a.b.c.SpecialService" singleton="false"/>
140 *   &lt;bean id="anotherService" class="a.b.c.AnotherService" singleton="false"/>
141 *
142 *   &lt;bean id="myServiceFactory"
143 *            class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
144 *     &lt;property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
145 *   &lt;/bean>
146 *
147 *   &lt;bean id="clientBean" class="a.b.c.MyClientBean">
148 *     &lt;property name="myServiceFactory" ref="myServiceFactory"/>
149 *   &lt;/bean>
150 *
151 *&lt;/beans></pre>
152 *
153 * <p>The attendant {@code MyClientBean} class implementation might then
154 * look something like this:
155 *
156 * <pre class="code">package a.b.c;
157 *
158 *public class MyClientBean {
159 *
160 *    private ServiceFactory myServiceFactory;
161 *
162 *    // actual implementation provided by the Spring container
163 *    public void setServiceFactory(ServiceFactory myServiceFactory) {
164 *        this.myServiceFactory = myServiceFactory;
165 *    }
166 *
167 *    public void someBusinessMethod() {
168 *        // get a 'fresh', brand new MyService instance
169 *        MyService service = this.myServiceFactory.getService("specialService");
170 *        // use the service object to effect the business logic...
171 *    }
172 *
173 *    public void anotherBusinessMethod() {
174 *        // get a 'fresh', brand new MyService instance
175 *        MyService service = this.myServiceFactory.getService("anotherService");
176 *        // use the service object to effect the business logic...
177 *    }
178 *}</pre>
179 *
180 * <p>See {@link ObjectFactoryCreatingFactoryBean} for an alternate approach.
181 *
182 * @author Colin Sampaleanu
183 * @author Juergen Hoeller
184 * @since 1.1.4
185 * @see #setServiceLocatorInterface
186 * @see #setServiceMappings
187 * @see ObjectFactoryCreatingFactoryBean
188 */
189public class ServiceLocatorFactoryBean implements FactoryBean<Object>, BeanFactoryAware, InitializingBean {
190
191        private Class<?> serviceLocatorInterface;
192
193        private Constructor<Exception> serviceLocatorExceptionConstructor;
194
195        private Properties serviceMappings;
196
197        private ListableBeanFactory beanFactory;
198
199        private Object proxy;
200
201
202        /**
203         * Set the service locator interface to use, which must have one or more methods with
204         * the signatures {@code MyType xxx()} or {@code MyType xxx(MyIdType id)}
205         * (typically, {@code MyService getService()} or {@code MyService getService(String id)}).
206         * See the {@link ServiceLocatorFactoryBean class-level Javadoc} for
207         * information on the semantics of such methods.
208         */
209        public void setServiceLocatorInterface(Class<?> interfaceType) {
210                this.serviceLocatorInterface = interfaceType;
211        }
212
213        /**
214         * Set the exception class that the service locator should throw if service
215         * lookup failed. The specified exception class must have a constructor
216         * with one of the following parameter types: {@code (String, Throwable)}
217         * or {@code (Throwable)} or {@code (String)}.
218         * <p>If not specified, subclasses of Spring's BeansException will be thrown,
219         * for example NoSuchBeanDefinitionException. As those are unchecked, the
220         * caller does not need to handle them, so it might be acceptable that
221         * Spring exceptions get thrown as long as they are just handled generically.
222         * @see #determineServiceLocatorExceptionConstructor
223         * @see #createServiceLocatorException
224         */
225        public void setServiceLocatorExceptionClass(Class<? extends Exception> serviceLocatorExceptionClass) {
226                if (serviceLocatorExceptionClass != null && !Exception.class.isAssignableFrom(serviceLocatorExceptionClass)) {
227                        throw new IllegalArgumentException(
228                                        "serviceLocatorException [" + serviceLocatorExceptionClass.getName() + "] is not a subclass of Exception");
229                }
230                this.serviceLocatorExceptionConstructor =
231                                determineServiceLocatorExceptionConstructor(serviceLocatorExceptionClass);
232        }
233
234        /**
235         * Set mappings between service ids (passed into the service locator)
236         * and bean names (in the bean factory). Service ids that are not defined
237         * here will be treated as bean names as-is.
238         * <p>The empty string as service id key defines the mapping for {@code null} and
239         * empty string, and for factory methods without parameter. If not defined,
240         * a single matching bean will be retrieved from the bean factory.
241         * @param serviceMappings mappings between service ids and bean names,
242         * with service ids as keys as bean names as values
243         */
244        public void setServiceMappings(Properties serviceMappings) {
245                this.serviceMappings = serviceMappings;
246        }
247
248        @Override
249        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
250                if (!(beanFactory instanceof ListableBeanFactory)) {
251                        throw new FatalBeanException(
252                                        "ServiceLocatorFactoryBean needs to run in a BeanFactory that is a ListableBeanFactory");
253                }
254                this.beanFactory = (ListableBeanFactory) beanFactory;
255        }
256
257        @Override
258        public void afterPropertiesSet() {
259                if (this.serviceLocatorInterface == null) {
260                        throw new IllegalArgumentException("Property 'serviceLocatorInterface' is required");
261                }
262
263                // Create service locator proxy.
264                this.proxy = Proxy.newProxyInstance(
265                                this.serviceLocatorInterface.getClassLoader(),
266                                new Class<?>[] {this.serviceLocatorInterface},
267                                new ServiceLocatorInvocationHandler());
268        }
269
270
271        /**
272         * Determine the constructor to use for the given service locator exception
273         * class. Only called in case of a custom service locator exception.
274         * <p>The default implementation looks for a constructor with one of the
275         * following parameter types: {@code (String, Throwable)}
276         * or {@code (Throwable)} or {@code (String)}.
277         * @param exceptionClass the exception class
278         * @return the constructor to use
279         * @see #setServiceLocatorExceptionClass
280         */
281        @SuppressWarnings("unchecked")
282        protected Constructor<Exception> determineServiceLocatorExceptionConstructor(Class<? extends Exception> exceptionClass) {
283                try {
284                        return (Constructor<Exception>) exceptionClass.getConstructor(String.class, Throwable.class);
285                }
286                catch (NoSuchMethodException ex) {
287                        try {
288                                return (Constructor<Exception>) exceptionClass.getConstructor(Throwable.class);
289                        }
290                        catch (NoSuchMethodException ex2) {
291                                try {
292                                        return (Constructor<Exception>) exceptionClass.getConstructor(String.class);
293                                }
294                                catch (NoSuchMethodException ex3) {
295                                        throw new IllegalArgumentException(
296                                                        "Service locator exception [" + exceptionClass.getName() +
297                                                        "] neither has a (String, Throwable) constructor nor a (String) constructor");
298                                }
299                        }
300                }
301        }
302
303        /**
304         * Create a service locator exception for the given cause.
305         * Only called in case of a custom service locator exception.
306         * <p>The default implementation can handle all variations of
307         * message and exception arguments.
308         * @param exceptionConstructor the constructor to use
309         * @param cause the cause of the service lookup failure
310         * @return the service locator exception to throw
311         * @see #setServiceLocatorExceptionClass
312         */
313        protected Exception createServiceLocatorException(Constructor<Exception> exceptionConstructor, BeansException cause) {
314                Class<?>[] paramTypes = exceptionConstructor.getParameterTypes();
315                Object[] args = new Object[paramTypes.length];
316                for (int i = 0; i < paramTypes.length; i++) {
317                        if (String.class == paramTypes[i]) {
318                                args[i] = cause.getMessage();
319                        }
320                        else if (paramTypes[i].isInstance(cause)) {
321                                args[i] = cause;
322                        }
323                }
324                return BeanUtils.instantiateClass(exceptionConstructor, args);
325        }
326
327
328        @Override
329        public Object getObject() {
330                return this.proxy;
331        }
332
333        @Override
334        public Class<?> getObjectType() {
335                return this.serviceLocatorInterface;
336        }
337
338        @Override
339        public boolean isSingleton() {
340                return true;
341        }
342
343
344        /**
345         * Invocation handler that delegates service locator calls to the bean factory.
346         */
347        private class ServiceLocatorInvocationHandler implements InvocationHandler {
348
349                @Override
350                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
351                        if (ReflectionUtils.isEqualsMethod(method)) {
352                                // Only consider equal when proxies are identical.
353                                return (proxy == args[0]);
354                        }
355                        else if (ReflectionUtils.isHashCodeMethod(method)) {
356                                // Use hashCode of service locator proxy.
357                                return System.identityHashCode(proxy);
358                        }
359                        else if (ReflectionUtils.isToStringMethod(method)) {
360                                return "Service locator: " + serviceLocatorInterface;
361                        }
362                        else {
363                                return invokeServiceLocatorMethod(method, args);
364                        }
365                }
366
367                private Object invokeServiceLocatorMethod(Method method, Object[] args) throws Exception {
368                        Class<?> serviceLocatorMethodReturnType = getServiceLocatorMethodReturnType(method);
369                        try {
370                                String beanName = tryGetBeanName(args);
371                                if (StringUtils.hasLength(beanName)) {
372                                        // Service locator for a specific bean name
373                                        return beanFactory.getBean(beanName, serviceLocatorMethodReturnType);
374                                }
375                                else {
376                                        // Service locator for a bean type
377                                        return beanFactory.getBean(serviceLocatorMethodReturnType);
378                                }
379                        }
380                        catch (BeansException ex) {
381                                if (serviceLocatorExceptionConstructor != null) {
382                                        throw createServiceLocatorException(serviceLocatorExceptionConstructor, ex);
383                                }
384                                throw ex;
385                        }
386                }
387
388                /**
389                 * Check whether a service id was passed in.
390                 */
391                private String tryGetBeanName(Object[] args) {
392                        String beanName = "";
393                        if (args != null && args.length == 1 && args[0] != null) {
394                                beanName = args[0].toString();
395                        }
396                        // Look for explicit serviceId-to-beanName mappings.
397                        if (serviceMappings != null) {
398                                String mappedName = serviceMappings.getProperty(beanName);
399                                if (mappedName != null) {
400                                        beanName = mappedName;
401                                }
402                        }
403                        return beanName;
404                }
405
406                private Class<?> getServiceLocatorMethodReturnType(Method method) throws NoSuchMethodException {
407                        Class<?>[] paramTypes = method.getParameterTypes();
408                        Method interfaceMethod = serviceLocatorInterface.getMethod(method.getName(), paramTypes);
409                        Class<?> serviceLocatorReturnType = interfaceMethod.getReturnType();
410
411                        // Check whether the method is a valid service locator.
412                        if (paramTypes.length > 1 || void.class == serviceLocatorReturnType) {
413                                throw new UnsupportedOperationException(
414                                                "May only call methods with signature '<type> xxx()' or '<type> xxx(<idtype> id)' " +
415                                                "on factory interface, but tried to call: " + interfaceMethod);
416                        }
417                        return serviceLocatorReturnType;
418                }
419        }
420
421}