001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.beans.factory.config;
018:
019: import java.lang.reflect.Constructor;
020: import java.lang.reflect.InvocationHandler;
021: import java.lang.reflect.InvocationTargetException;
022: import java.lang.reflect.Method;
023: import java.lang.reflect.Proxy;
024: import java.util.Properties;
025:
026: import org.springframework.beans.BeanUtils;
027: import org.springframework.beans.BeansException;
028: import org.springframework.beans.FatalBeanException;
029: import org.springframework.beans.factory.BeanFactory;
030: import org.springframework.beans.factory.BeanFactoryAware;
031: import org.springframework.beans.factory.BeanFactoryUtils;
032: import org.springframework.beans.factory.FactoryBean;
033: import org.springframework.beans.factory.InitializingBean;
034: import org.springframework.beans.factory.ListableBeanFactory;
035: import org.springframework.util.StringUtils;
036:
037: /**
038: * A {@link org.springframework.beans.factory.FactoryBean} implementation that
039: * takes an interface which must have one or more methods with
040: * the signatures <code>MyType xxx()</code> or <code>MyType xxx(MyIdType id)</code>
041: * (typically, <code>MyService getService()</code> or <code>MyService getService(String id)</code>)
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</code> 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</code>. The resulting String can be used as bean name as-is,
069: * provided that corresponding beans are defined in the bean factory.
070: * Alternatively, {@link #setServiceMappings(java.util.Properties) a custom mapping}
071: * 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 dependant 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"><beans>
087: *
088: * <!-- Prototype bean since we have state -->
089: * <bean id="myService" class="a.b.c.MyService" singleton="false"/>
090: *
091: * <!-- will lookup the above 'myService' bean by *TYPE* -->
092: * <bean id="myServiceFactory"
093: * class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
094: * <property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
095: * </bean>
096: *
097: * <bean id="clientBean" class="a.b.c.MyClientBean">
098: * <property name="myServiceFactory" ref="myServiceFactory"/>
099: * </bean>
100: *
101: *</beans></pre>
102: *
103: * <p>The attendant <code>MyClientBean</code> 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 dependant 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"><beans>
139: *
140: * <!-- Prototype beans since we have state (both extend MyService) -->
141: * <bean id="specialService" class="a.b.c.SpecialService" singleton="false"/>
142: * <bean id="anotherService" class="a.b.c.AnotherService" singleton="false"/>
143: *
144: * <bean id="myServiceFactory"
145: * class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
146: * <property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
147: * </bean>
148: *
149: * <bean id="clientBean" class="a.b.c.MyClientBean">
150: * <property name="myServiceFactory" ref="myServiceFactory"/>
151: * </bean>
152: *
153: *</beans></pre>
154: *
155: * <p>The attendant <code>MyClientBean</code> 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: */
191: public class ServiceLocatorFactoryBean implements FactoryBean,
192: BeanFactoryAware, InitializingBean {
193:
194: private Class serviceLocatorInterface;
195:
196: private Constructor serviceLocatorExceptionConstructor;
197:
198: private Properties serviceMappings;
199:
200: private ListableBeanFactory beanFactory;
201:
202: private Object proxy;
203:
204: /**
205: * Set the service locator interface to use, which must have one or more methods with
206: * the signatures <code>MyType xxx()</code> or <code>MyType xxx(MyIdType id)</code>
207: * (typically, <code>MyService getService()</code> or <code>MyService getService(String id)</code>).
208: * See the {@link ServiceLocatorFactoryBean class-level Javadoc} for
209: * information on the semantics of such methods.
210: *
211: * @param interfaceType the {@link java.lang.Class} of the interface to be used for the service locator
212: */
213: public void setServiceLocatorInterface(Class interfaceType) {
214: this .serviceLocatorInterface = interfaceType;
215: }
216:
217: /**
218: * Set the exception class that the service locator should throw if service
219: * lookup failed. The specified exception class must have a constructor
220: * with one of the following parameter types: <code>(String, Throwable)</code>
221: * or <code>(Throwable)</code> or <code>(String)</code>.
222: * <p>If not specified, subclasses of Spring's BeansException will be thrown,
223: * for example NoSuchBeanDefinitionException. As those are unchecked, the
224: * caller does not need to handle them, so it might be acceptable that
225: * Spring exceptions get thrown as long as they are just handled generically.
226: *
227: * @see #determineServiceLocatorExceptionConstructor(Class)
228: * @see #createServiceLocatorException(java.lang.reflect.Constructor,org.springframework.beans.BeansException)
229: * @see org.springframework.beans.BeansException
230: * @see org.springframework.beans.factory.NoSuchBeanDefinitionException
231: */
232: public void setServiceLocatorExceptionClass(
233: Class serviceLocatorExceptionClass) {
234: if (serviceLocatorExceptionClass != null
235: && !Exception.class
236: .isAssignableFrom(serviceLocatorExceptionClass)) {
237: throw new IllegalArgumentException(
238: "serviceLocatorException ["
239: + serviceLocatorExceptionClass.getName()
240: + "] is not a subclass of Exception");
241: }
242: this .serviceLocatorExceptionConstructor = determineServiceLocatorExceptionConstructor(serviceLocatorExceptionClass);
243: }
244:
245: /**
246: * Set mappings between service ids (passed into the service locator)
247: * and bean names (in the bean factory). Service ids that are not defined
248: * here will be treated as bean names as-is.
249: * <p>The empty string as service id key defines the mapping for <code>null</code> and
250: * empty string, and for factory methods without parameter. If not defined,
251: * a single matching bean will be retrieved from the bean factory.
252: *
253: * @param serviceMappings mappings between service ids and bean names,
254: * with service ids as keys as bean names as values
255: */
256: public void setServiceMappings(Properties serviceMappings) {
257: this .serviceMappings = serviceMappings;
258: }
259:
260: public void setBeanFactory(BeanFactory beanFactory)
261: throws BeansException {
262: if (!(beanFactory instanceof ListableBeanFactory)) {
263: throw new FatalBeanException(
264: "ServiceLocatorFactoryBean needs to run in a BeanFactory that is a ListableBeanFactory");
265: }
266: this .beanFactory = (ListableBeanFactory) beanFactory;
267: }
268:
269: public void afterPropertiesSet() {
270: if (this .serviceLocatorInterface == null) {
271: throw new IllegalArgumentException(
272: "serviceLocatorInterface is required");
273: }
274:
275: // Create service locator proxy.
276: this .proxy = Proxy.newProxyInstance(
277: this .serviceLocatorInterface.getClassLoader(),
278: new Class[] { this .serviceLocatorInterface },
279: new ServiceLocatorInvocationHandler());
280: }
281:
282: /**
283: * Determine the constructor to use for the given service locator exception
284: * class. Only called in case of a custom service locator exception.
285: * <p>The default implementation looks for a constructor with one of the
286: * following parameter types: <code>(String, Throwable)</code>
287: * or <code>(Throwable)</code> or <code>(String)</code>.
288: *
289: * @param exceptionClass the exception class
290: * @return the constructor to use
291: * @see #setServiceLocatorExceptionClass
292: */
293: protected Constructor determineServiceLocatorExceptionConstructor(
294: Class exceptionClass) {
295: try {
296: return exceptionClass.getConstructor(new Class[] {
297: String.class, Throwable.class });
298: } catch (NoSuchMethodException ex) {
299: try {
300: return exceptionClass
301: .getConstructor(new Class[] { Throwable.class });
302: } catch (NoSuchMethodException ex2) {
303: try {
304: return exceptionClass
305: .getConstructor(new Class[] { String.class });
306: } catch (NoSuchMethodException ex3) {
307: throw new IllegalArgumentException(
308: "serviceLocatorException ["
309: + exceptionClass.getName()
310: + "] neither has a (String, Throwable) constructor nor a (String) constructor");
311: }
312: }
313: }
314: }
315:
316: /**
317: * Create a service locator exception for the given cause.
318: * Only called in case of a custom service locator exception.
319: * <p>The default implementation can handle all variations of
320: * message and exception arguments.
321: *
322: * @param exceptionConstructor the constructor to use
323: * @param cause the cause of the service lookup failure
324: * @return the service locator exception to throw
325: * @see #setServiceLocatorExceptionClass
326: */
327: protected Exception createServiceLocatorException(
328: Constructor exceptionConstructor, BeansException cause) {
329: Class[] paramTypes = exceptionConstructor.getParameterTypes();
330: Object[] args = new Object[paramTypes.length];
331: for (int i = 0; i < paramTypes.length; i++) {
332: if (paramTypes[i].equals(String.class)) {
333: args[i] = cause.getMessage();
334: } else if (paramTypes[i].isInstance(cause)) {
335: args[i] = cause;
336: }
337: }
338: return (Exception) BeanUtils.instantiateClass(
339: exceptionConstructor, args);
340: }
341:
342: public Object getObject() {
343: return this .proxy;
344: }
345:
346: public Class getObjectType() {
347: return this .serviceLocatorInterface;
348: }
349:
350: public boolean isSingleton() {
351: return true;
352: }
353:
354: /**
355: * Invocation handler that delegates service locator calls to the bean factory.
356: */
357: private class ServiceLocatorInvocationHandler implements
358: InvocationHandler {
359:
360: public Object invoke(Object proxy, Method method, Object[] args)
361: throws Throwable {
362: Object service;
363: if (Object.class.equals(method.getDeclaringClass())) {
364: service = invokeStandardObjectMethod(method, args);
365: } else {
366: service = invokeServiceLocatorMethod(method, args);
367: }
368: return service;
369: }
370:
371: public String toString() {
372: return "Service locator: "
373: + serviceLocatorInterface.getName();
374: }
375:
376: private Object invokeServiceLocatorMethod(Method method,
377: Object[] args) throws Exception {
378: Class serviceLocatorMethodReturnType = getServiceLocatorMethodReturnType(method);
379: try {
380: String beanName = tryGetBeanName(args);
381: if (StringUtils.hasLength(beanName)) {
382: // Service locator for a specific bean name.
383: return beanFactory.getBean(beanName,
384: serviceLocatorMethodReturnType);
385: } else {
386: // Service locator for a bean type.
387: return BeanFactoryUtils
388: .beanOfTypeIncludingAncestors(beanFactory,
389: serviceLocatorMethodReturnType);
390: }
391: } catch (BeansException ex) {
392: if (serviceLocatorExceptionConstructor != null) {
393: throw createServiceLocatorException(
394: serviceLocatorExceptionConstructor, ex);
395: }
396: throw ex;
397: }
398: }
399:
400: /**
401: * Check whether a service id was passed in.
402: */
403: private String tryGetBeanName(Object[] args) {
404: String beanName = "";
405: if (args != null && args.length == 1 && args[0] != null) {
406: beanName = args[0].toString();
407: }
408: // Look for explicit serviceId-to-beanName mappings.
409: if (serviceMappings != null) {
410: String mappedName = serviceMappings
411: .getProperty(beanName);
412: if (mappedName != null) {
413: beanName = mappedName;
414: }
415: }
416: return beanName;
417: }
418:
419: private Class getServiceLocatorMethodReturnType(Method method)
420: throws NoSuchMethodException {
421: Class[] paramTypes = method.getParameterTypes();
422: Method interfaceMethod = serviceLocatorInterface.getMethod(
423: method.getName(), paramTypes);
424: Class serviceLocatorReturnType = interfaceMethod
425: .getReturnType();
426:
427: // Check whether the method is a valid service locator.
428: if (paramTypes.length > 1
429: || void.class.equals(serviceLocatorReturnType)) {
430: throw new UnsupportedOperationException(
431: "May only call methods with signature '<type> xxx()' or '<type> xxx(<idtype> id)' "
432: + "on factory interface, but tried to call: "
433: + interfaceMethod);
434: }
435: return serviceLocatorReturnType;
436: }
437:
438: /**
439: * It's normal to get here for non service locator interface method calls
440: * (toString, equals, etc.). Simply apply the call to the invocation handler object.
441: */
442: private Object invokeStandardObjectMethod(Method method,
443: Object[] args) throws Throwable {
444: try {
445: return method.invoke(this , args);
446: } catch (InvocationTargetException invEx) {
447: throw invEx.getTargetException();
448: }
449: }
450: }
451:
452: }
|