├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── linkedkeeper │ └── spi │ └── ExtensionLoader.java └── test ├── java └── demo │ ├── ExtensionSPITest.java │ ├── JavaSPITest.java │ ├── SpringSPITest.java │ ├── java │ ├── Bumblebee.java │ ├── OptimusPrime.java │ └── Robot.java │ └── spring │ ├── AOrderService.java │ ├── BOrderService.java │ ├── COrderService.java │ ├── OrderService.java │ ├── WareService.java │ └── WareServiceImpl.java └── resources ├── META-INF ├── services │ └── demo.java.Robot ├── spi │ └── demo.spring.OrderService └── spring.factories └── spring.xml /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Francis Zhang 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spi-imp 2 | 基于 SPI 原理实现自定义动态 Bean 加载到 Spring 容器的实现逻辑 3 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | spi-imp 8 | com.linkedkeeper 9 | 1.0-SNAPSHOT 10 | jar 11 | 12 | pi-imp 13 | 14 | 15 | UTF-8 16 | 17 | 18 | 19 | 20 | org.slf4j 21 | slf4j-log4j12 22 | 1.7.7 23 | 24 | 25 | 26 | org.springframework 27 | spring-context 28 | 3.2.12.RELEASE 29 | 30 | 31 | org.springframework 32 | spring-test 33 | 3.2.12.RELEASE 34 | test 35 | 36 | 37 | junit 38 | junit 39 | 4.10 40 | test 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-compiler-plugin 49 | 3.1 50 | 51 | 1.7 52 | 1.7 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-source-plugin 58 | 2.2.1 59 | 60 | 61 | attach-sources 62 | 63 | jar 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/java/com/linkedkeeper/spi/ExtensionLoader.java: -------------------------------------------------------------------------------- 1 | package com.linkedkeeper.spi; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.beans.factory.support.BeanDefinitionBuilder; 5 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 6 | import org.springframework.beans.factory.support.DefaultListableBeanFactory; 7 | import org.springframework.beans.factory.support.GenericBeanDefinition; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.ApplicationContextAware; 10 | import org.springframework.context.ConfigurableApplicationContext; 11 | import org.springframework.util.StringUtils; 12 | 13 | import java.io.BufferedReader; 14 | import java.io.InputStreamReader; 15 | import java.net.URL; 16 | import java.nio.charset.StandardCharsets; 17 | import java.util.Enumeration; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import java.util.concurrent.ConcurrentMap; 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | public class ExtensionLoader implements ApplicationContextAware { 27 | 28 | private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class); 29 | 30 | private ApplicationContext context; 31 | 32 | private static final ConcurrentMap, Map> EXTENSION_LOADERS = new ConcurrentHashMap<>(); 33 | private static final String SERVICES_DIRECTORY = "META-INF/spi/"; 34 | 35 | public Map getExtensionLoader(Class type) { 36 | if (type == null) { 37 | throw new IllegalArgumentException("Extension type == null"); 38 | } 39 | if (!type.isInterface()) { 40 | throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!"); 41 | } 42 | Map loader = EXTENSION_LOADERS.get(type); 43 | if (loader == null) { 44 | synchronized (ExtensionLoader.class) { 45 | loader = EXTENSION_LOADERS.get(type); 46 | if (loader == null) { 47 | EXTENSION_LOADERS.putIfAbsent(type, loadExtensionClass(type.getName())); 48 | loader = EXTENSION_LOADERS.get(type); 49 | } 50 | } 51 | } 52 | return loader; 53 | } 54 | 55 | private Map loadExtensionClass(String type) { 56 | Map extensionClasses = new HashMap<>(); 57 | loadDirectory(extensionClasses, SERVICES_DIRECTORY, type); 58 | return extensionClasses; 59 | } 60 | 61 | private void loadDirectory(Map extensionClasses, String dir, String type) { 62 | String fileName = dir + type; 63 | try { 64 | Enumeration urls; 65 | ClassLoader classLoader = findClassLoader(); 66 | if (classLoader != null) { 67 | urls = classLoader.getResources(fileName); 68 | } else { 69 | urls = ClassLoader.getSystemResources(fileName); 70 | } 71 | if (urls != null) { 72 | while (urls.hasMoreElements()) { 73 | URL resourcesURL = urls.nextElement(); 74 | loadResources(extensionClasses, classLoader, resourcesURL); 75 | } 76 | } 77 | } catch (Throwable t) { 78 | logger.error("Exception occurred when loading extension class (interface: " + 79 | type + ", description file: " + fileName + ").", t); 80 | } 81 | } 82 | 83 | private void loadResources(Map extensionClasses, ClassLoader classLoader, URL resourceURL) { 84 | try { 85 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) { 86 | String line; 87 | while ((line = reader.readLine()) != null) { 88 | final int ci = line.indexOf('#'); 89 | if (ci >= 0) { 90 | line = line.substring(0, ci); 91 | } 92 | line = line.trim(); 93 | if (line.length() > 0) { 94 | try { 95 | String name = null; 96 | int i = line.indexOf('='); 97 | if (i > 0) { 98 | name = line.substring(0, i).trim(); 99 | line = line.substring(i + 1).trim(); 100 | } 101 | if (line.length() > 0) { 102 | loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); 103 | } 104 | } catch (Throwable t) { 105 | IllegalStateException e = new IllegalStateException("Failed to load extension class (class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t); 106 | } 107 | } 108 | } 109 | } 110 | } catch (Throwable t) { 111 | logger.error("Exception occurred when loading extension class (class file: " + resourceURL + ") in " + resourceURL, t); 112 | } 113 | } 114 | 115 | private static ClassLoader findClassLoader() { 116 | return DefaultListableBeanFactory.class.getClassLoader(); 117 | } 118 | 119 | private void loadClass(Map extensionClasses, java.net.URL resourceURL, Class clazz, String name) throws NoSuchMethodException { 120 | if (StringUtils.isEmpty(name)) { 121 | throw new IllegalStateException("No such extension name for the class " + name + " in the config " + resourceURL); 122 | } 123 | saveInExtensionClass(extensionClasses, clazz, name); 124 | } 125 | 126 | private void saveInExtensionClass(Map extensionClasses, Class clazz, String name) { 127 | Object o = extensionClasses.get(name); 128 | if (o == null) { 129 | Object bean = parseClassToSpringBean(name, clazz); 130 | extensionClasses.put(name, bean); 131 | } else { 132 | throw new IllegalStateException("Duplicate extension name " + name + " on " + clazz.getName() + " and " + clazz.getName()); 133 | } 134 | } 135 | 136 | private Object parseClassToSpringBean(String name, Class obj) { 137 | String beanName = obj.getSimpleName().concat(name); 138 | 139 | BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(obj); 140 | GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition(); 141 | definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_NAME); 142 | getRegistry().registerBeanDefinition(beanName, definition); 143 | 144 | return context.getBean(beanName); 145 | } 146 | 147 | public BeanDefinitionRegistry getRegistry() { 148 | ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) context; 149 | return (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); 150 | } 151 | 152 | @Override 153 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 154 | this.context = applicationContext; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/demo/ExtensionSPITest.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import com.linkedkeeper.spi.ExtensionLoader; 4 | import demo.spring.OrderService; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 10 | 11 | 12 | @RunWith(SpringJUnit4ClassRunner.class) 13 | @ContextConfiguration({"classpath:spring.xml"}) 14 | public class ExtensionSPITest { 15 | 16 | @Autowired 17 | private ExtensionLoader loader; 18 | 19 | @Test 20 | public void sayHello() throws Exception { 21 | OrderService japan = (OrderService) loader.getExtensionLoader(OrderService.class).get("japan"); 22 | japan.getOrder("hello."); 23 | OrderService china = (OrderService) loader.getExtensionLoader(OrderService.class).get("china"); 24 | china.getOrder("worder."); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/demo/JavaSPITest.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import demo.java.Robot; 4 | import org.junit.Test; 5 | 6 | import java.util.ServiceLoader; 7 | 8 | public class JavaSPITest { 9 | 10 | @Test 11 | public void sayHello() throws Exception { 12 | ServiceLoader serviceLoader = ServiceLoader.load(Robot.class); 13 | System.out.println("Java SPI"); 14 | for (Robot robot : serviceLoader) { 15 | robot.sayHello(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/demo/SpringSPITest.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | 4 | import demo.spring.OrderService; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.core.io.support.SpringFactoriesLoader; 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 10 | 11 | import java.util.List; 12 | 13 | @RunWith(SpringJUnit4ClassRunner.class) 14 | @ContextConfiguration({"classpath:spring.xml"}) 15 | public class SpringSPITest { 16 | 17 | @Test 18 | public void sayHello() throws Exception { 19 | List services = SpringFactoriesLoader.loadFactories(OrderService.class, null); 20 | for (OrderService service : services) { 21 | service.getOrder("worder."); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/demo/java/Bumblebee.java: -------------------------------------------------------------------------------- 1 | package demo.java; 2 | 3 | public class Bumblebee implements Robot { 4 | 5 | @Override 6 | public void sayHello() { 7 | System.out.println("Hello, I am Bumblebee."); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/demo/java/OptimusPrime.java: -------------------------------------------------------------------------------- 1 | package demo.java; 2 | 3 | public class OptimusPrime implements Robot { 4 | 5 | @Override 6 | public void sayHello() { 7 | System.out.println("Hello, I am Optimus Prime."); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/demo/java/Robot.java: -------------------------------------------------------------------------------- 1 | package demo.java; 2 | 3 | public interface Robot { 4 | 5 | void sayHello(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/demo/spring/AOrderService.java: -------------------------------------------------------------------------------- 1 | package demo.spring; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | public class AOrderService implements OrderService { 6 | 7 | @Autowired 8 | private WareService wareService; 9 | 10 | @Override 11 | public void getOrder(String msg) { 12 | wareService.getWare("a order -> " + msg); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/demo/spring/BOrderService.java: -------------------------------------------------------------------------------- 1 | package demo.spring; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | public class BOrderService implements OrderService { 6 | 7 | @Autowired 8 | private WareService wareService; 9 | 10 | @Override 11 | public void getOrder(String msg) { 12 | wareService.getWare("b order -> " + msg); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/demo/spring/COrderService.java: -------------------------------------------------------------------------------- 1 | package demo.spring; 2 | 3 | public class COrderService implements OrderService { 4 | 5 | @Override 6 | public void getOrder(String msg) { 7 | System.out.println("c order -> " + msg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/demo/spring/OrderService.java: -------------------------------------------------------------------------------- 1 | package demo.spring; 2 | 3 | public interface OrderService { 4 | 5 | void getOrder(String msg); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/demo/spring/WareService.java: -------------------------------------------------------------------------------- 1 | package demo.spring; 2 | 3 | public interface WareService { 4 | 5 | void getWare(String msg); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/demo/spring/WareServiceImpl.java: -------------------------------------------------------------------------------- 1 | package demo.spring; 2 | 3 | public class WareServiceImpl implements WareService { 4 | 5 | @Override 6 | public void getWare(String msg) { 7 | System.out.println("getWare print: " + msg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/services/demo.java.Robot: -------------------------------------------------------------------------------- 1 | demo.java.OptimusPrime 2 | demo.java.Bumblebee -------------------------------------------------------------------------------- /src/test/resources/META-INF/spi/demo.spring.OrderService: -------------------------------------------------------------------------------- 1 | china=demo.spring.AOrderService 2 | japan=demo.spring.BOrderService -------------------------------------------------------------------------------- /src/test/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | demo.spring.OrderService=demo.spring.COrderService,demo.spring.AOrderService -------------------------------------------------------------------------------- /src/test/resources/spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------