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