From 988f3767525daab7913e403e0d18dd6cf5fe3c81 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 7 Sep 2012 17:00:22 +0200 Subject: [PATCH] Added SpringFactoriesLoader --- .../core/SpringFactoriesLoader.java | 186 ++++++++++++++++++ .../springframework/core/DummyFactory.java | 28 +++ .../springframework/core/MyDummyFactory1.java | 33 ++++ .../springframework/core/MyDummyFactory2.java | 33 ++++ .../core/SpringFactoriesLoaderTests.java | 45 +++++ .../springFactoriesLoaderTests.properties | 2 + 6 files changed, 327 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/core/SpringFactoriesLoader.java create mode 100644 spring-core/src/test/java/org/springframework/core/DummyFactory.java create mode 100644 spring-core/src/test/java/org/springframework/core/MyDummyFactory1.java create mode 100644 spring-core/src/test/java/org/springframework/core/MyDummyFactory2.java create mode 100644 spring-core/src/test/java/org/springframework/core/SpringFactoriesLoaderTests.java create mode 100644 spring-core/src/test/resources/org/springframework/core/springFactoriesLoaderTests.properties diff --git a/spring-core/src/main/java/org/springframework/core/SpringFactoriesLoader.java b/spring-core/src/main/java/org/springframework/core/SpringFactoriesLoader.java new file mode 100644 index 0000000000..e6fc7e3a04 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SpringFactoriesLoader.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * General purpose factory loading mechanism. + * + *

The {@code SpringFactoriesLoader} loads and instantiates factories of a given type + * from a given file location. If a location is not given, the {@linkplain + * #DEFAULT_FACTORIES_LOCATION default location} is used. + * + *

The file should be in {@link Properties} format, where the keys is the fully + * qualified interface or abstract class name, and the value is a comma separated list of + * implementations. For instance: + *

+ * example.MyService=example.MyServiceImpl1,example.MyServiceImpl2
+ * 
+ * where {@code MyService} is the name of the interface, and {@code MyServiceImpl1} and + * {@code MyServiceImpl2} are the two implementations. + * + * @author Arjen Poutsma + * @since 3.2 + */ +public abstract class SpringFactoriesLoader { + + private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class); + + /** The location to look for the factories. Can be present in multiple JAR files. */ + public static final String DEFAULT_FACTORIES_LOCATION = "META-INF/spring.factories"; + + /** + * Loads the factory implementations of the given type from the default location, using + * the given class loader. + * + *

The returned factories are ordered in accordance with the {@link OrderComparator}. + * + * @param factoryClass the interface or abstract class representing the factory + * @param classLoader the ClassLoader to use for loading, can be {@code null} to use the + * default + * @throws IllegalArgumentException in case of errors + */ + public static List loadFactories(Class factoryClass, + ClassLoader classLoader) { + return loadFactories(factoryClass, classLoader, null); + } + + /** + * Loads the factory implementations of the given type from the given location, using the + * given class loader. + * + *

The returned factories are ordered in accordance with the {@link OrderComparator}. + * + * @param factoryClass the interface or abstract class representing the factory + * @param classLoader the ClassLoader to use for loading, can be {@code null} to + * use the default + * @param factoriesLocation the factories file location, can be {@code null} to use the + * {@linkplain #DEFAULT_FACTORIES_LOCATION default} + * @throws IllegalArgumentException in case of errors + */ + public static List loadFactories(Class factoryClass, + ClassLoader classLoader, + String factoriesLocation) { + Assert.notNull(factoryClass, "'factoryClass' must not be null"); + + if (classLoader == null) { + classLoader = ClassUtils.getDefaultClassLoader(); + } + if (factoriesLocation == null) { + factoriesLocation = DEFAULT_FACTORIES_LOCATION; + } + + List factoryNames = + loadFactoryNames(factoryClass, classLoader, factoriesLocation); + + if (logger.isTraceEnabled()) { + logger.trace( + "Loaded [" + factoryClass.getName() + "] names: " + factoryNames); + } + + List result = new ArrayList(factoryNames.size()); + for (String factoryName : factoryNames) { + result.add(instantiateFactory(factoryName, factoryClass, classLoader)); + } + + Collections.sort(result, new OrderComparator()); + + return result; + } + + private static List loadFactoryNames(Class factoryClass, + ClassLoader classLoader, + String factoriesLocation) { + + String factoryClassName = factoryClass.getName(); + + try { + List result = new ArrayList(); + Enumeration urls = classLoader.getResources(factoriesLocation); + while (urls.hasMoreElements()) { + URL url = (URL) urls.nextElement(); + Properties properties = + PropertiesLoaderUtils.loadProperties(new UrlResource(url)); + String factoryClassNames = properties.getProperty(factoryClassName); + result.addAll(Arrays.asList( + StringUtils.commaDelimitedListToStringArray(factoryClassNames))); + } + return result; + } + catch (IOException ex) { + throw new IllegalArgumentException( + "Unable to load [" + factoryClass.getName() + + "] factories from location [" + + factoriesLocation + "]", ex); + } + + + } + + @SuppressWarnings("unchecked") + private static T instantiateFactory(String instanceClassName, + Class factoryClass, + ClassLoader classLoader) { + try { + Class instanceClass = ClassUtils.forName(instanceClassName, classLoader); + if (!factoryClass.isAssignableFrom(instanceClass)) { + throw new IllegalArgumentException( + "Class [" + instanceClassName + "] is not assignable to [" + + factoryClass.getName() + "]"); + } + return (T) instanceClass.newInstance(); + } + catch (ClassNotFoundException ex) { + throw new IllegalArgumentException( + factoryClass.getName() + " class [" + instanceClassName + + "] not found", ex); + } + catch (LinkageError err) { + throw new IllegalArgumentException( + "Invalid " + factoryClass.getName() + " class [" + instanceClassName + + "]: problem with handler class file or dependent class", err); + } + catch (InstantiationException ex) { + throw new IllegalArgumentException( + "Could not instantiate bean class [" + instanceClassName + + "]: Is it an abstract class?", ex); + } + catch (IllegalAccessException ex) { + throw new IllegalArgumentException( + "Could not instantiate bean class [" + instanceClassName + + "Is the constructor accessible?", ex); + } + } + +} \ No newline at end of file diff --git a/spring-core/src/test/java/org/springframework/core/DummyFactory.java b/spring-core/src/test/java/org/springframework/core/DummyFactory.java new file mode 100644 index 0000000000..d19257a79a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/DummyFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core; + +/** + * Used by {@link SpringFactoriesLoaderTests} + * + * @author Arjen Poutsma + */ +public interface DummyFactory { + + String getString(); + +} diff --git a/spring-core/src/test/java/org/springframework/core/MyDummyFactory1.java b/spring-core/src/test/java/org/springframework/core/MyDummyFactory1.java new file mode 100644 index 0000000000..f8d2a122f0 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/MyDummyFactory1.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core; + +/** + * Used by {@link SpringFactoriesLoaderTests} + * + * @author Arjen Poutsma + */ +public class MyDummyFactory1 implements DummyFactory, Ordered { + + public int getOrder() { + return 1; + } + + public String getString() { + return "Foo"; + } +} diff --git a/spring-core/src/test/java/org/springframework/core/MyDummyFactory2.java b/spring-core/src/test/java/org/springframework/core/MyDummyFactory2.java new file mode 100644 index 0000000000..0a7d5f6245 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/MyDummyFactory2.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core; + +/** + * Used by {@link org.springframework.core.SpringFactoriesLoaderTests} + * + * @author Arjen Poutsma + */ +public class MyDummyFactory2 implements DummyFactory, Ordered { + + public int getOrder() { + return 2; + } + + public String getString() { + return "Bar"; + } +} diff --git a/spring-core/src/test/java/org/springframework/core/SpringFactoriesLoaderTests.java b/spring-core/src/test/java/org/springframework/core/SpringFactoriesLoaderTests.java new file mode 100644 index 0000000000..07fb50fbc6 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/SpringFactoriesLoaderTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +/** @author Arjen Poutsma */ +public class SpringFactoriesLoaderTests { + + @Test + public void loadFactories() { + List factories = SpringFactoriesLoader + .loadFactories(DummyFactory.class, null, + "org/springframework/core/springFactoriesLoaderTests.properties"); + + assertEquals(2, factories.size()); + assertTrue(factories.get(0) instanceof MyDummyFactory1); + assertTrue(factories.get(1) instanceof MyDummyFactory2); + } + + @Test(expected = IllegalArgumentException.class) + public void loadInvalid() { + SpringFactoriesLoader.loadFactories(String.class, null, + "org/springframework/core/springFactoriesLoaderTests.properties"); + } + +} diff --git a/spring-core/src/test/resources/org/springframework/core/springFactoriesLoaderTests.properties b/spring-core/src/test/resources/org/springframework/core/springFactoriesLoaderTests.properties new file mode 100644 index 0000000000..a2813e2969 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/core/springFactoriesLoaderTests.properties @@ -0,0 +1,2 @@ +org.springframework.core.DummyFactory=org.springframework.core.MyDummyFactory2,org.springframework.core.MyDummyFactory1 +java.lang.String=org.springframework.core.MyDummyFactory1 \ No newline at end of file