SpringBoot-插件化以及springboot扩展接口

插件化常用的实现思路

  • 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
在这里插入图片描述

为什么会出现这个效果呢?因为我们在实现类配置了具体使用哪一种方式进行短信的发送,而加载插件的时候正好能够找到对应的服务实现,这样的话就给当前的业务提供了一个较好的扩展点。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/79343.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript中的Generator函数及其使用方式

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ Generator函数⭐ 创建Generator函数⭐ 调用Generator函数⭐ Generator函数的应用1. 异步编程2. 生成器&#xff08;Generator&#xff09; ⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧…

老板要我开发一个简单的工作流引擎-读后感与补充

概述 最近读了一篇《老板要我开发一个简单的工作流引擎》 幽默风趣&#xff0c;干货较多&#xff0c;作为流程引擎的设计者、开发者、探索者&#xff0c;写的很好&#xff0c;合计自己的理解&#xff0c;对每个功能补充说明&#xff0c;对于流程引擎的应用场景&#xff0c;做出…

vue中slot,slot-scope,v-slot的用法和区别

slot用于设置标签的属性值(slot“title”)slot-scopev-slot slot <el-menu-item v-if"!navMenu.children" :key"navMenu.id" :index"navMenu.id " click"itemClick(navMenu)" ><span slot"title">{{ navMenu.…

无涯教程-JavaScript - N函数

描述 N函数返回一个转换为数字的值。 语法 N (value) 争论 Argument描述Required/OptionalValue 要转换的值或对值的引用。 N转换下表中列出的值。 Required 值 N的返回值一个数字那个数字日期,采用Microsoft Excel中可用的内置日期格式之一该日期的序列号 TRUE 1 FALSE…

大数据课程L3——网站流量项目的系统搭建

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解网站流量项目的运行环境; ⚪ 了解网站流量项目的日志采集系统搭建; ⚪ 了解网站流量项目的离线业务系统搭建; ⚪ 了解网站流量项目的Hive做离线数据处理; ⚪ 了解网站流量项目的…

Java笔记:Java线程Dump分析

1 Thread Dump介绍 1.1 什么是Thread Dump Thread Dump是非常有用的诊断Java应用问题的工具。每一个Java虚拟机都有及时生成所有线程在某一点状态的thread-dump的能力&#xff0c;虽然各个 Java虚拟机打印的thread dump略有不同&#xff0c;但是 大多都提供了当前活动线程的快…

【深度学习】 Python 和 NumPy 系列教程(廿七):Matplotlib详解:3、多子图和布局:散点矩阵图(Scatter Matrix Plot)

目录 一、前言 二、实验环境 三、Matplotlib详解 1、2d绘图类型 2、3d绘图类型 3、多子图和布局 1. subplot()函数 2. subplots()函数 3. 散点矩阵图&#xff08;Scatter Matrix Plot&#xff09; 一、前言 Python是一种高级编程语言&#xff0c;由Guido van Rossum于…

Web服务器解析:从基础到高级的全面指南

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 引言 Web服务器是现代互…

前端实现符合Promise/A+规范的Promise

&#x1f3ac; 岸边的风&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 介绍&#xff1a; Promise/A规范简介 1. Promise的三种状态&#xff1a; 2. 状态转换&#xff1a; 3. Promise的…

盘点11种高效改进卷积神经网络(CNN)的优化方法【核心代码下载】

卷积作为神经网络的核心计算之一&#xff0c;在CV领域有着诸多突破性进展&#xff0c;因而近年来关于卷积神经网络的研究不断。由于卷积的计算十分复杂&#xff0c;而且神经网络运行时很大一部分时间都会耗费在计算卷积上&#xff0c;因此优化卷积计算就显得尤为重要。 那么如…

2023谷歌开发者大会直播详细脚本

主播:三掌柜 设备:手机+直播云台 平台:CSDN 角度:对Google技术感兴趣的人、技术爱好者 画风:言简意赅、通俗易懂,将难懂的内容转化为简洁的描述,旨在让每一位观众都能有所收获。 形式:直播互动,提高受众人群的范围,包括但不限于对Google感兴趣的任何人,以及对G…

【LeetCode-简单题】剑指 Offer 58 - II. 左旋转字符串

文章目录 题目方法一&#xff1a;连续双指针翻转 题目 方法一&#xff1a;连续双指针翻转 class Solution {public String reverseLeftWords(String s, int n) {StringBuffer sb new StringBuffer(s);reverseWord(sb,0,n-1);reverseWord(sb,n,sb.length()-1);return sb.revers…

OLED透明屏触控:引领未来科技革命的创新力量

OLED透明屏触控技术作为一项颠覆性的创新&#xff0c;正在引领新一轮科技革命。它将OLED显示技术与触摸技术相结合&#xff0c;实现了透明度和触控功能的完美融合。 在这篇文章中&#xff0c;尼伽将通过引用最新的市场数据、报告和行业动态&#xff0c;详细介绍OLED透明屏触控…

hutool的HttpRequest.post的使用-包括上传文档等多个传参【总结版本】

首先hutool已经为我们封装好了远程调用的接口&#xff0c;我们只要将对应的传参和方式对应填写即可 hutool官方文档 1实际应用 post 常见的使用json传参&#xff0c;contend type为application/json RequestMapping("login") ResponseBody public static String s…

用于非线性多载波卫星信道的多输入多输出符号速率信号数字预失真器DPD(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【Docker】ubuntu20.04 X86机器搭建NVIDIA ARM64 TX2的Docker镜像

文章目录 1. 设置ubuntu为清华源1.1 备份源文件1.2 替换清华源1.3 更新清华源 2. Ubuntu Docker 安装3. 安装qemu4. 安装Nvidia TX2 Docker镜像5. 如何使用TX2容器6. 参考资料 1. 设置ubuntu为清华源 为了后面ubuntu下载安装软件快些&#xff0c;需要使用国内的源&#xff0c;…

conda常用命令及问题解决-创建虚拟环境

好久没写博文了&#xff0c;感觉在学习的过程中还是要注意积累与分享&#xff0c;这样利人利己。 conda包清理&#xff0c;许多无用的包是很占用空间的 conda clean -p //删除没有用的包 conda clean -y -all //删除pkgs目录下所有的无用安装包及cacheconda创建虚拟环境…

文件上传漏洞(CVE-2022-30887)

简介 多语言药房管理系统&#xff08;MPMS&#xff09;是用PHP和MySQL开发的&#xff0c;该软件的主要目的是在药房和客户之间提供一套接口&#xff0c;客户是该软件的主要用户。该软件有助于为药房业务创建一个综合数据库&#xff0c;并根据到期、产品等各种参数提供各种报告…

Flutter插件之阿里百川

上一篇&#xff1a;Flutter插件的制作和发布&#xff0c;我们已经了解了如何制作一个通用的双端插件&#xff0c;本篇就带领大家将阿里百川双端sdk制作成一个flutter插件供项目调用&#xff01; 目录 登录并打开控制台&#xff0c;创建应用&#xff1a;填写应用相关信息开通百川…

华为云云服务器云耀L实例评测 | 智能不卡顿:如何实现流畅的业务运行

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…