插件化常用的实现思路
- spi机制,Service Provider Interface ,是JDK内置的一种服务发现机制,SPI是一种动态替换扩展机制
- 约定配置和目录,利用反射配合实现
- springboot中的Factories机制
- Java agent(探针)技术
- Spring内置扩展点
- 第三方插件包,例如:spring-plugin-core
- spring aop技术
SPI实现插件化案例
一、目录结构如下
二、自定义接口及其实现类
/**接口
*/
public interface MessagePlugin{public String sendMsg(Map msgMap);
}
/**AliyunMsg实现类
*/
public class AliyunMsg implements MessagePlugin{@Overridepublic String sendMsg(Map msgMap){System.out.println("aliyun sendMsg");return "aliyun sendMsg";}
}
/**TencentMsg实现类
*/
public class TencentMsg implements MessagePlugin {@Overridepublic String sendMsg(Map msgMap) {System.out.println("tencent sendMsg");return "tencent sendMsg";}
}
三、在resources目录下按照规范要求创建文件目录,并填写实现类的全类名
文件目录规范为接口所在的包全类名。
四、自定义服务加载类
使用ServiceLoader的方式可以加载到不同接口的实现,业务中只需要根据自身的需求,结合配置参数的方式就可以灵活的控制具体使用哪一个实现。
public static void main(String[] args){ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);Interator<MessagePlugin> iterator = serviceLoader.iterator();Map map = new HashMap();while(iterator.hasNext()){MessagePlugin messagePlugin = iterator.next();messagePlugin.sendMsg(map);}
}
注意:serviceloader有缺陷,使用中必须在META-INF里定义接口名称的文件,在文件中才能写上实现类的类名。 如果一个项目里插件化的东西太多,可能会出现越来越多配置文件。
所以对上面的缺陷调整
1、配置文件中,将具体的实现类配置进去
server:port: 8081impl:name: com.michael.plugins.spi.MessagePluginclazz: - com.michael.plugins.impl.TencentMsg- com.michale.plugins.impl.AliyunMsg
2、自定义配置文件加载类
通过该类,将上述配置文件中的实现类封装到类对象中,方便后续使用
@ConfigurationProperties("impl")
@ToString
public class ClassImpl{@Getter@SetterString name;@Getter@SetterString[] clazz;
}
3、自定义测试接口
@RestController
public class SendMsgController{@AutowiredClassImpl classImpl;@GetMapping("/sendMsg")public String sendMsg() throws Exception{for(int i =0;i < classImpl.getClazz().length; i++){Class pluginClass = Class.forName(classImpl.getClazz()[i]);MessagePlugin messagePlugin = (MessagePlugin) pluginClass.newInstance();messagePlugin.sendMsg(new HashMap());}return "success";}
}
4、启动类
@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
public class PluginApp{public static void main(String[] args) {SpringApplication.run(PluginApp.class,args);}
}
启动工程代码后,调用接口:localhost:8081/sendMsg,在控制台中可以看到下面的输出信息,即通过这种方式也可以实现类似serviceloader的方式,不过在实际使用时,可以结合配置参数进行灵活的控制;
在很多场景下,可能我们并不想直接在工程中引入接口实现的依赖包,这时候可以考虑通过读取指定目录下的依赖jar的方式,利用反射的方式进行动态加载,这也是生产中一种比较常用的实践经验
继续调整
1、创建约定目录
在当前工程下创建一个lib目录,并将依赖jar放进去
2、新增读取jar的工具类
添加一个工具类,用于读取指定目录下的jar,通过反射的方式,结合配置文件中的约定配置进行反射方法的执行
@Component
public class ServiceLoaderUtils{@AutowiredClassImpl classImpl;public static void loadJarFromAppFolder() throws Exception{String path = "E:\\code-self\\bitzpp\\lib";File f = new File(path);if(f.isDirectory()){for(File subf:f.listFiles()){loadJarFile(subf);}}else{loadJarFile(f);}}public static void loadJarFile(File path) throws Exception{URL url = path.toURI().toURL();URLClassLoader classLoader = (URLClassLoader).ClassLoader.getSystemClassLoader();//加载//Method method = URLClassLoader.class.getDeclareMethod("sendMsg",Map.class);Method method = URLClassLoader.class.getMethod("sendMsg",Map.class);method.setAccessible(true);method.invoke(classLoader,url);}public void main(String[] args) throws Exception{System.out.println(invokeMethod("hello"));;}public String doExecuteMethod() throws Exception{String path = "E:\\code-self\\bitzpp\\lib";File f1 = new File(path);Object result = null;if (f1.isDirectory()) {for (File subf : f1.listFiles()) {//获取文件名称String name = subf.getName();String fullPath = path + "\\" + name;//执行反射相关的方法//ServiceLoaderUtils serviceLoaderUtils = new ServiceLoaderUtils();//result = serviceLoaderUtils.loadMethod(fullPath);File f = new File(fullPath);URL urlB = f.toURI().toURL();URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread().getContextClassLoader());String[] clazz = classImpl.getClazz();for(String claName : clazz){if(name.equals("biz-pt-1.0-SNAPSHOT.jar")){if(!claName.equals("com.congge.spi.BitptImpl")){continue;}Class<?> loadClass = classLoaderA.loadClass(claName);if(Objects.isNull(loadClass)){continue;}//获取实例Object obj = loadClass.newInstance();Map map = new HashMap();//获取方法Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);result = method.invoke(obj,map);if(Objects.nonNull(result)){break;}}else if(name.equals("miz-pt-1.0-SNAPSHOT.jar")){if(!claName.equals("com.congge.spi.MizptImpl")){continue;}Class<?> loadClass = classLoaderA.loadClass(claName);if(Objects.isNull(loadClass)){continue;}//获取实例Object obj = loadClass.newInstance();Map map = new HashMap();//获取方法Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);result = method.invoke(obj,map);if(Objects.nonNull(result)){break;}}}if(Objects.nonNull(result)){break;}}}return result.toString();}public Object loadMethod(String fullPath) throws Exception{File f = new File(fullPath);URL urlB = f.toURI().toURL();URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread().getContextClassLoader());Object result = null;String[] clazz = classImpl.getClazz();for(String claName : clazz){Class<?> loadClass = classLoaderA.loadClass(claName);if(Objects.isNull(loadClass)){continue;}//获取实例Object obj = loadClass.newInstance();Map map = new HashMap();//获取方法Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);result = method.invoke(obj,map);if(Objects.nonNull(result)){break;}}return result;}public static String invokeMethod(String text) throws Exception{String path = "E:\\code-self\\bitzpp\\lib\\miz-pt-1.0-SNAPSHOT.jar";File f = new File(path);URL urlB = f.toURI().toURL();URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread().getContextClassLoader());Class<?> product = classLoaderA.loadClass("com.congge.spi.MizptImpl");//获取实例Object obj = product.newInstance();Map map = new HashMap();//获取方法Method method=product.getDeclaredMethod("sendMsg",Map.class);//执行方法Object result1 = method.invoke(obj,map);// TODO According to the requirements , write the implementation code.return result1.toString();}public static String getApplicationFolder() {String path = ServiceLoaderUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath();return new File(path).getParent();}}
3、添加测试接口
@GetMapping("/sendMsgV2")
public String index() throws Exception{String result = serviceLoaderUtils.doExecuteMethod();return result;
}
以上全部完成之后,启动工程,测试一下该接口,仍然可以得到预期结果;
SpringBoot插件化实现
其实框架自身提供了非常多的扩展点,其中最适合做插件扩展的莫过于spring.factories的实现;
在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化,这种自定义的SPI机制是Spring Boot Starter实现的基础。
spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
- loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表;
- loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表;
上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下:
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {String factoryClassName = factoryClass.getName();try {Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));List<String> result = new ArrayList<String>();while (urls.hasMoreElements()) {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 [" + FACTORIES_RESOURCE_LOCATION + "]", ex);}
}
在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件,就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖
spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:
com.xxx.interface=com.xxx.classname
如果一个接口希望配置多个实现类,可以使用’,’进行分割
一、定义一个服务接口以及两个服务实现类
public interface SmsPlugin{public void sendMessage(String message);
}
public class BizSmsImpl implements SmsPlugin {@Overridepublic void sendMessage(String message) {System.out.println("this is BizSmsImpl sendMessage..." + message);}
}
public class SystemSmsImpl implements SmsPlugin {@Overridepublic void sendMessage(String message) {System.out.println("this is SystemSmsImpl sendMessage..." + message);}
}
二、添加spring.factories文件
在resources目录下,创建一个名叫:META-INF的目录,然后在该目录下定义一个spring.factories的配置文件,内容如下,其实就是配置了服务接口,以及两个实现类的全类名的路径;
com.congge.plugin.spi.SmsPlugin=\
com.congge.plugin.impl.SystemSmsImpl,\
com.congge.plugin.impl.BizSmsImpl
三、添加自定义接口
这里和java 的spi有点类似,只不过是这里换成了SpringFactoriesLoader去加载服务;
@GetMapping("/sendMsgV3")
public String sendMsgV3(String msg) throws Exception{List<SmsPlugin> smsServices = SpringFactoriesLoader.loadFactories();for(SmsPlugin smsService : smsServices){smsService.sendMessage(msg);}return "success";
}
启动工程之后,调用一下该接口进行测试,localhost:8087/sendMsgV3?msg=hello,通过控制台,可以看到,这种方式能够正确获取到系统中可用的服务实现;
利用spring的这种机制,可以很好的对系统中的某些业务逻辑通过插件化接口的方式进行扩展实现;
SpringBoot扩展接口
可扩展接口的启动调用顺序
一、ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer
这是整个spring容器在刷新之前初始化ConfigurableApplicationContext的回调接口
容器刷新之前调用此类的initialize方法。这个点允许被用户自己扩展。用户可以在整个spring容器还没被初始化之前做一些事情。可以想到的场景可能为,在最开始激活一些配置,或者利用这时候class还没被类加载器加载的时机,进行动态字节码注入等操作。
扩展方式:此时spring容器还没被初始化,所以想要自己扩展有三种方式
- 在启动类中用springApplication.addInitializers(new TestApplicationContextInitializer())加入
- 配置文件中context.initializer.classes=com.example.demo.TestApplicationContextInitializer
- Spring SPI扩展,在spring.factories中加入org.springframework.context.ApplicationContextInitializer=com.example.demo.TestApplicationContextInitializer
public class TestApplicationContextInitializer implements ApplicationContextInitializer{@Overridepublic void initialize(ConfigurableApplicationContext applicationContext){System.out.println("[ApplicationContextInitializer]");}
}
二、BeanDefinitionRegistryPostProcessor
org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
这个接口在读取项目中的beanDefinition之后执行,提供一个补充的扩展点
使用场景:你可以在这里动态注册自己的beanDefinition,可以加载classpath之外的bean
public class TestBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { System.out.println("[BeanDefinitionRegistryPostProcessor] postProcessBeanDefinitionRegistry"); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { System.out.println("[BeanDefinitionRegistryPostProcessor] postProcessBeanFactory"); }
}
三、BeanFactoryPostProcessor
org.springframework.beans.factory.config.BeanFactoryPostProcessor
这个接口是beanFactory的扩展接口,调用时机在spring在读取beanDefinition信息之后,实例化bean之前
用户可以通过实现这个扩展接口来自行处理一些东西,比如修改已经注册的beanDefinition的元信息。
public class TestBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { System.out.println("[BeanFactoryPostProcessor]"); }
}
四、InstantiationAwareBeanPostProcessor
org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor
该接口继承了BeanPostProcess接口,区别如下:
BeanPostProcess接口只在bean的初始化阶段进行扩展(注入spring上下文前后)
InstantiationAwareBeanPostProcessor接口在此基础上增加了3个方法,把可扩展的范围增加了实例化阶段和属性注入阶段。
- postProcessBeforeInstantiation:实例化bean之前,相当于new这个bean之前
- postProcessAfterInstantiation:实例化bean之后,相当于new这个bean之后
- postProcessPropertyValues:bean已经实例化完成,在属性注入时阶段触发,@Autowired,@Resource等注解原理基于此方法实现
- postProcessBeforeInitialization:初始化bean之前,相当于把bean注入spring上下文之前
- postProcessAfterInitialization:初始化bean之后,相当于把bean注入spring上下文之后
使用场景:这个扩展点非常有用 ,无论是写中间件和业务中,都能利用这个特性。比如对实现了某一类接口的bean在各个生命期间进行收集,或者对某个类型的bean进行统一的设值等等。
public class TestInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("[TestInstantiationAwareBeanPostProcessor] before initialization " + beanName); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("[TestInstantiationAwareBeanPostProcessor] after initialization " + beanName); return bean; } @Override public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { System.out.println("[TestInstantiationAwareBeanPostProcessor] before instantiation " + beanName); return null; } @Override public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { System.out.println("[TestInstantiationAwareBeanPostProcessor] after instantiation " + beanName); return true; } @Override public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { System.out.println("[TestInstantiationAwareBeanPostProcessor] postProcessPropertyValues " + beanName); return pvs; }
五、SmartInstantiationAwareBeanPostProcessor
org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor
该扩展接口有3个触发点方法:
public class TestSmartInstantiationAwareBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor { @Override public Class<?> predictBeanType(Class<?> beanClass, String beanName) throws BeansException { System.out.println("[TestSmartInstantiationAwareBeanPostProcessor] predictBeanType " + beanName); return beanClass; } @Override public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, String beanName) throws BeansException { System.out.println("[TestSmartInstantiationAwareBeanPostProcessor] determineCandidateConstructors " + beanName); return null; } @Override public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { System.out.println("[TestSmartInstantiationAwareBeanPostProcessor] getEarlyBeanReference " + beanName); return bean; }
}
六、BeanFactoryAware
org.springframework.beans.factory.BeanFactoryAware
这个类只有一个触发点,发生在bean的实例化之后,注入属性之前,也就是Setter之前。这个类的扩展点方法为setBeanFactory,可以拿到BeanFactory这个属性。
使用场景为,你可以在bean实例化之后,但还未初始化之前,拿到 BeanFactory,在这个时候,可以对每个bean作特殊化的定制。也或者可以把BeanFactory拿到进行缓存,日后使用。
public class TestBeanFactoryAware implements BeanFactoryAware { @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("[TestBeanFactoryAware] " + beanFactory.getBean(TestBeanFactoryAware.class).getClass().getSimpleName()); }
}
七、ApplicationContextAwareProcessor
org.springframework.context.support.ApplicationContextAwareProcessor
该类本身并没有扩展点,但是该类内部却有6个扩展点可供实现 ,这些类触发的时机在bean实例化之后,初始化之前
可以看到,该类用于执行各种驱动接口,在bean实例化之后,属性填充之后,通过执行以上红框标出的扩展接口,来获取对应容器的变量。所以这里应该来说是有6个扩展点,这里就放一起来说了
八、BeanNameAware
org.springframework.beans.factory.BeanNameAware
可以看到,这个类也是Aware扩展的一种,触发点在bean的初始化之前,也就是postProcessBeforeInitialization之前,这个类的触发点方法只有一个:setBeanName
使用场景为:用户可以扩展这个点,在初始化bean之前拿到spring容器中注册的的beanName,来自行修改这个beanName的值。
public class NormalBeanA implements BeanNameAware{ public NormalBeanA() { System.out.println("NormalBean constructor"); } @Override public void setBeanName(String name) { System.out.println("[BeanNameAware] " + name); }
}
九、@PostConstruct
javax.annotation.PostConstruct
这个并不算一个扩展点,其实就是一个标注。其作用是在bean的初始化阶段,如果对一个方法标注了@PostConstruct,会先调用这个方法。这里重点是要关注下这个标准的触发点,这个触发点是在postProcessBeforeInitialization之后,InitializingBean.afterPropertiesSet之前。
使用场景:用户可以对某一方法进行标注,来进行初始化某一个属性
public class NormalBeanA { public NormalBeanA() { System.out.println("NormalBean constructor"); } @PostConstruct public void init(){ System.out.println("[PostConstruct] NormalBeanA"); }
}
十、InitializingBean
org.springframework.beans.factory.InitializingBean
这个类,顾名思义,也是用来初始化bean的。InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。这个扩展点的触发时机在postProcessAfterInitialization之前。
使用场景:用户实现此接口,来进行系统启动的时候一些业务指标的初始化工作。
public class NormalBeanA implements InitializingBean{ @Override public void afterPropertiesSet() throws Exception { System.out.println("[InitializingBean] NormalBeanA"); }
}
十一、FactoryBean
org.springframework.beans.factory.FactoryBean
一般情况下,Spring通过反射机制利用bean的class属性指定支线类去实例化bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在bean中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。
FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为FactoryBean的形式
使用场景:用户可以扩展这个类,来为要实例化的bean作一个代理,比如为该对象的所有的方法作一个拦截,在调用前后输出一行log,模仿ProxyFactoryBean的功能。
public class TestFactoryBean implements FactoryBean<TestFactoryBean.TestFactoryInnerBean> { @Override public TestFactoryBean.TestFactoryInnerBean getObject() throws Exception { System.out.println("[FactoryBean] getObject"); return new TestFactoryBean.TestFactoryInnerBean(); } @Override public Class<?> getObjectType() { return TestFactoryBean.TestFactoryInnerBean.class; } @Override public boolean isSingleton() { return true; } public static class TestFactoryInnerBean{ }
}
十二、SmartInitializingSingleton
org.springframework.beans.factory.SmartInitializingSingleton
这个接口中只有一个方法afterSingletonsInstantiated,其作用是是 在spring容器管理的所有单例对象(非懒加载对象)初始化完成之后调用的回调接口。其触发时机为postProcessAfterInitialization之后。
使用场景:用户可以扩展此接口在对所有单例对象初始化完毕后,做一些后置的业务处理。
public class TestSmartInitializingSingleton implements SmartInitializingSingleton { @Override public void afterSingletonsInstantiated() { System.out.println("[TestSmartInitializingSingleton]"); }
}
十三、CommandLineRunner
org.springframework.boot.CommandLineRunner
这个接口也只有一个方法:run(String… args),触发时机为整个项目启动完毕后,自动执行。如果有多个CommandLineRunner,可以利用@Order来进行排序。
使用场景:用户扩展此接口,进行启动项目之后一些业务的预处理。
public class TestCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("[TestCommandLineRunner]"); }
}
十四、DisposableBean
org.springframework.beans.factory.DisposableBean
这个扩展点也只有一个方法:destroy(),其触发时机为当此对象销毁时,会自动执行这个方法。比如说运行applicationContext.registerShutdownHook时,就会触发这个方法。
public class NormalBeanA implements DisposableBean { @Override public void destroy() throws Exception { System.out.println("[DisposableBean] NormalBeanA"); }
}
十五、ApplicationListener
org.springframework.context.ApplicationListener
准确的说,这个应该不算spring&springboot当中的一个扩展点,ApplicationListener可以监听某个事件的event,触发时机可以穿插在业务方法执行过程中,用户可以自定义某个业务事件。
但是spring内部也有一些内置事件,这种事件,可以穿插在启动调用中。我们也可以利用这个特性,来自己做一些内置事件的监听器来达到和前面一些触发点大致相同的事情。
接下来罗列下spring主要的内置事件:
插件化机制案例实战
案例背景
- 3个微服务模块,在A模块中有个插件化的接口;
- 在A模块中的某个接口,需要调用插件化的服务实现进行短信发送;
- 可以通过配置文件配置参数指定具体的哪一种方式发送短信;
- 如果没有加载到任何插件,将走A模块在默认的发短信实现;
模块结构
- biz-pp,插件化接口工程;定义服务接口,并提供出去jar被其他实现工程依赖
- bitpt,aliyun短信发送实现;依赖biz-pp的jar并实现SPI中的方法,按照API规范实现完成后,打成jar包,或者安装到仓库中
- miz-pt,tencent短信发送实现;依赖biz-pp的jar并实现SPI中的方法,按照API规范实现完成后,打成jar包,或者安装到仓库中
biz-pp在pom中依赖bitpt与miz-pt的jar,或者通过启动加载的方式即可得到具体某个实现;
一、biz-app添加服务接口,并打成jar安装到仓库
public interface MessagePlugin{public String sendMsg(Map msgMap);
}
自定义服务加载工具类
/**这个类,可以理解为在真实的业务编码中,可以根据业务定义的规则,具体加载哪个插件的实现类进行发送短信的操作;
*/
public class PluginFactory {public void installPlugin(){Map context = new LinkedHashMap();context.put("_userId","");context.put("_version","1.0");context.put("_type","sms");ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);Iterator<MessagePlugin> iterator = serviceLoader.iterator();while (iterator.hasNext()){MessagePlugin messagePlugin = iterator.next();messagePlugin.sendMsg(context);}}public static MessagePlugin getTargetPlugin(String type){ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);Iterator<MessagePlugin> iterator = serviceLoader.iterator();List<MessagePlugin> messagePlugins = new ArrayList<>();while (iterator.hasNext()){MessagePlugin messagePlugin = iterator.next();messagePlugins.add(messagePlugin);}MessagePlugin targetPlugin = null;for (MessagePlugin messagePlugin : messagePlugins) {boolean findTarget = false;switch (type) {case "aliyun":if (messagePlugin instanceof BitptImpl){targetPlugin = messagePlugin;findTarget = true;break;}case "tencent":if (messagePlugin instanceof MizptImpl){targetPlugin = messagePlugin;findTarget = true;break;}}if(findTarget) break;}return targetPlugin;}public static void main(String[] args) {new PluginFactory().installPlugin();}}
自定义Controller接口、Service
@RestController
public class SmsController {@Autowiredprivate SmsService smsService;@Autowiredprivate ServiceLoaderUtils serviceLoaderUtils;//localhost:8087/sendMsg?msg=sendMsg@GetMapping("/sendMsg")public String sendMessage(String msg){return smsService.sendMsg(msg);}}
@Service
public class SmsService {@Value("${msg.type}")private String msgType;@Autowiredprivate DefaultSmsService defaultSmsService;public String sendMsg(String msg) {MessagePlugin messagePlugin = PluginFactory.getTargetPlugin(msgType);Map paramMap = new HashMap();if(Objects.nonNull(messagePlugin)){return messagePlugin.sendMsg(paramMap);}return defaultSmsService.sendMsg(paramMap);}
}
在该模块中,需要引入对具体实现的两个工程的jar依赖(也可以通过启动加载的命令方式)
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--依赖具体的实现--><dependency><groupId>com.congge</groupId><artifactId>biz-pt</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.congge</groupId><artifactId>miz-pt</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies>
二、插件化具体实现
<dependencies><dependency><groupId>com.congge</groupId><artifactId>biz-app</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
添加MessagePlugin接口的实现
public class BitptImpl implements MessagePlugin {@Overridepublic String sendMsg(Map msgMap) {Object userId = msgMap.get("userId");Object type = msgMap.get("_type");//TODO 参数校验System.out.println(" ==== userId :" + userId + ",type :" + type);System.out.println("aliyun send message success");return "aliyun send message success";}
}
按照前文的方式,在resources目录下创建一个文件,注意文件名称为SPI中的接口全名,文件内容为实现类的全类名
com.congge.spi.BitptImpl
完成实现类的编码后,通过maven命令将jar安装到仓库中,然后再在上一步的biz-app中引入即可;
启动biz-app服务,调用接口:localhost:8087/sendMsg?msg=sendMsg
为什么会出现这个效果呢?因为我们在实现类配置了具体使用哪一种方式进行短信的发送,而加载插件的时候正好能够找到对应的服务实现,这样的话就给当前的业务提供了一个较好的扩展点。