2026/2/10 2:34:34
网站建设
项目流程
杭州自助建站模板,建设学校网站需要具备,多地优化防控举措方便民众生活,婚庆公司简介模板01前言1.1 使用插件的好处1.1.1 模块解耦实现服务模块之间解耦的方式有很多#xff0c;但是插件来说#xff0c;其解耦的程度似乎更高#xff0c;而且更灵活#xff0c;可定制化、个性化更好。举例来说#xff0c;代码中可以使用设计模式来选择使用哪种方式发送短信给下单…01前言1.1 使用插件的好处1.1.1 模块解耦实现服务模块之间解耦的方式有很多但是插件来说其解耦的程度似乎更高而且更灵活可定制化、个性化更好。举例来说代码中可以使用设计模式来选择使用哪种方式发送短信给下单完成的客户问题是各个短信服务商并不一定能保证在任何情况下都能发送成功怎么办呢这时候设计模式也没法帮你解决这个问题如果使用定制化插件的方式结合外部配置参数假设系统中某种短信发送不出去了这时候就可以利用插件动态植入切换为不同的厂商发短信了。1.1.2 提升扩展性和开放性以spring来说之所以具备如此广泛的生态与其自身内置的各种可扩展的插件机制是分不开的试想为什么使用了spring框架之后可以很方便的对接其他中间件那就是spring框架提供了很多基于插件化的扩展点。插件化机制让系统的扩展性得以提升从而可以丰富系统的周边应用生态。1.1.3 方便第三方接入有了插件之后第三方应用或系统如果要对接自身的系统直接基于系统预留的插件接口完成一套适合自己业务的实现即可而且对自身系统的侵入性很小甚至可以实现基于配置参数的热加载方便灵活开箱即用。1.2 插件化常用实现思路以java为例这里结合实际经验整理一些常用的插件化实现思路spi机制约定配置和目录利用反射配合实现springboot中的Factories机制java agent探针技术spring内置扩展点第三方插件包例如spring-plugin-corespring aop技术02Java常用插件实现方案2.1 serviceloader方式serviceloader是java提供的spi模式的实现。按照接口开发实现类而后配置java通过ServiceLoader来实现统一接口不同实现的依次调用。而java中最经典的serviceloader的使用就是Java的spi机制。2.1.1 java spiSPI全称 Service Provider Interface 是JDK内置的一种服务发现机制SPI是一种动态替换扩展机制比如有个接口你想在运行时动态给他添加实现你只需按照规范给他添加一个实现类即可。比如大家熟悉的jdbc中的Driver接口不同的厂商可以提供不同的实现有mysql的也有oracle的而Java的SPI机制就可以为某个接口寻找服务的实现。下面用一张简图说明下SPI机制的原理2.1.2 java spi 简单案例如下工程目录在某个应用工程中定义一个插件接口而其他应用工程为了实现这个接口只需要引入当前工程的jar包依赖进行实现即可这里为了演示我就将不同的实现直接放在同一个工程下定义接口public interface MessagePlugin { public String sendMsg(Map msgMap); }定义两个不同的实现public class AliyunMsg implements MessagePlugin { Override public String sendMsg(Map msgMap) { System.out.println(aliyun sendMsg); returnaliyun sendMsg; } } public class TencentMsg implements MessagePlugin { Override public String sendMsg(Map msgMap) { System.out.println(tencent sendMsg); returntencent sendMsg; } }在resources目录按照规范要求创建文件目录并填写实现类的全类名自定义服务加载类public static void main(String[] args) { ServiceLoaderMessagePlugin serviceLoader ServiceLoader.load(MessagePlugin.class); IteratorMessagePlugin iterator serviceLoader.iterator(); Map map new HashMap(); while (iterator.hasNext()){ MessagePlugin messagePlugin iterator.next(); messagePlugin.sendMsg(map); } }运行上面的程序后可以看到下面的效果这就是说使用ServiceLoader的方式可以加载到不同接口的实现业务中只需要根据自身的需求结合配置参数的方式就可以灵活的控制具体使用哪一个实现。2.2 自定义配置约定方式serviceloader其实是有缺陷的在使用中必须在META-INF里定义接口名称的文件在文件中才能写上实现类的类名如果一个项目里插件化的东西比较多那很可能会出现越来越多配置文件的情况。所以在结合实际项目使用时可以考虑下面这种实现思路A应用定义接口B,C,D等其他应用定义服务实现B,C,D应用实现后达成SDK的jarA应用引用SDK或者将SDK放到某个可以读取到的目录下A应用读取并解析SDK中的实现类在上文中案例基础上我们做如下调整2.2.1 添加配置文件在配置文件中将具体的实现类配置进去server : port : 8081 impl: name : com.congge.plugins.spi.MessagePlugin clazz : - com.congge.plugins.impl.TencentMsg - com.congge.plugins.impl.AliyunMsg2.2.2 自定义配置文件加载类通过这个类将上述配置文件中的实现类封装到类对象中方便后续使用ConfigurationProperties(impl) ToString public class ClassImpl { Getter Setter String name; Getter Setter String[] clazz; }2.2.3 自定义测试接口使用上述的封装对象通过类加载的方式动态的在程序中引入RestController public class SendMsgController { Autowired ClassImpl classImpl; //localhost:8081/sendMsg GetMapping(/sendMsg) public String sendMsg() throws Exception{ for (int i0;iclassImpl.getClazz().length;i) { Class pluginClass Class.forName(classImpl.getClazz()[i]); MessagePlugin messagePlugin (MessagePlugin) pluginClass.newInstance(); messagePlugin.sendMsg(new HashMap()); } returnsuccess; } }2.2.4 启动类EnableConfigurationProperties({ClassImpl.class}) SpringBootApplication public class PluginApp { public static void main(String[] args) { SpringApplication.run(PluginApp.class,args); } }启动工程代码后调用接口localhost:8081/sendMsg在控制台中可以看到下面的输出信息即通过这种方式也可以实现类似serviceloader的方式不过在实际使用时可以结合配置参数进行灵活的控制2.3 自定义配置读取依赖jar的方式更进一步在很多场景下可能我们并不想直接在工程中引入接口实现的依赖包这时候可以考虑通过读取指定目录下的依赖jar的方式利用反射的方式进行动态加载这也是生产中一种比较常用的实践经验。具体实践来说主要为下面的步骤应用A定义服务接口应用B,C,D等实现接口或者在应用内部实现相同的接口应用B,C,D打成jar放到应用A约定的读取目录下应用A加载约定目录下的jar通过反射加载目标方法在上述的基础上按照上面的实现思路来实现一下2.3.1 创建约定目录在当前工程下创建一个lib目录并将依赖的jar放进去2.3.2 新增读取jar的工具类添加一个工具类用于读取指定目录下的jar并通过反射的方式结合配置文件中的约定配置进行反射方法的执行Component public class ServiceLoaderUtils { Autowired ClassImpl classImpl; public static void loadJarsFromAppFolder() throws Exception { String path E:\\code-self\\bitzpp\\lib; File f new File(path); if (f.isDirectory()) { for (File subf : f.listFiles()) { if (subf.isFile()) { loadJarFile(subf); } } } else { loadJarFile(f); } } public static void loadJarFile(File path) throws Exception { URL url path.toURI().toURL(); // 可以获取到AppClassLoader可以提到前面不用每次都获取一次 URLClassLoader classLoader (URLClassLoader) ClassLoader.getSystemClassLoader(); // 加载 //Method method URLClassLoader.class.getDeclaredMethod(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 methodloadClass.getDeclaredMethod(sendMsg,Map.class); result method.invoke(obj,map); if(Objects.nonNull(result)){ break; } }elseif(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 methodloadClass.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 methodloadClass.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 methodproduct.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(); } }2.3.3 添加测试接口添加如下测试接口GetMapping(/sendMsgV2) public String index() throws Exception { String result serviceLoaderUtils.doExecuteMethod(); return result; }以上全部完成之后启动工程测试一下该接口仍然可以得到预期结果在上述的实现中还是比较粗糙的实际运用时还需要做较多的优化改进以满足实际的业务需要比如接口传入类型参数用于控制具体使用哪个依赖包的方法进行执行等03SpringBoot中的插件化实现在大家使用较多的springboot框架中其实框架自身提供了非常多的扩展点其中最适合做插件扩展的莫过于spring.factories的实现3.1 Spring Boot中的SPI机制在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称然后在程序中读取这些配置文件并实例化这种自定义的SPI机制是Spring Boot Starter实现的基础。3.2 Spring Factories实现原理spring-core包里定义了SpringFactoriesLoader类这个类实现了检索META-INF/spring.factories文件并获取指定接口的配置的功能。在这个类中定义了两个对外的方法loadFactories 根据接口类获取其实现类的实例这个方法返回的是对象列表loadFactoryNames 根据接口获取其接口类的名称这个方法返回的是类名的列表上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件并解析得到类名列表具体代码如下public static ListString loadFactoryNames(Class? factoryClass, ClassLoader classLoader) { String factoryClassName factoryClass.getName(); try { EnumerationURL urls (classLoader ! null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); ListString result new ArrayListString(); 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.interfacecom.xxx.classname如果一个接口希望配置多个实现类可以使用’,’进行分割3.3 Spring Factories案例实现接下来看一个具体的案例实现来体验下Spring Factories的使用3.3.1 定义一个服务接口自定义一个接口里面添加一个方法public interface SmsPlugin { public void sendMessage(String message); }3.3.2 定义2个服务实现实现类1public class BizSmsImpl implements SmsPlugin { Override public void sendMessage(String message) { System.out.println(this is BizSmsImpl sendMessage... message); } }实现类2public class SystemSmsImpl implements SmsPlugin { Override public void sendMessage(String message) { System.out.println(this is SystemSmsImpl sendMessage... message); } }3.3.3 添加spring.factories文件在resources目录下创建一个名叫META-INF的目录然后在该目录下定义一个spring.factories的配置文件内容如下其实就是配置了服务接口以及两个实现类的全类名的路径com.congge.plugin.spi.SmsPlugin\ com.congge.plugin.impl.SystemSmsImpl,\ com.congge.plugin.impl.BizSmsImpl3.3.4 添加自定义接口添加一个自定义的接口有没有发现这里和java 的spi有点类似只不过是这里换成了SpringFactoriesLoader去加载服务GetMapping(/sendMsgV3) public String sendMsgV3(String msg) throws Exception{ ListSmsPlugin smsServices SpringFactoriesLoader.loadFactories(SmsPlugin.class, null); for(SmsPlugin smsService : smsServices){ smsService.sendMessage(msg); } return success; }启动工程之后调用一下该接口进行测试localhost:8087/sendMsgV3?msghello通过控制台可以看到这种方式能够正确获取到系统中可用的服务实现利用spring的这种机制可以很好的对系统中的某些业务逻辑通过插件化接口的方式进行扩展实现04插件化机制案例实战结合上面掌握的理论知识下面基于Java SPI机制进行一个接近真实使用场景的完整的操作步骤4.1 案例背景3个微服务模块在A模块中有个插件化的接口在A模块中的某个接口需要调用插件化的服务实现进行短信发送可以通过配置文件配置参数指定具体的哪一种方式发送短信如果没有加载到任何插件将走A模块在默认的发短信实现4.1.1 模块结构1、biz-pp插件化接口工程2、bitptaliyun短信发送实现3、miz-pttencent短信发送实现4.1.2 整体实现思路本案例完整的实现思路参考如下biz-pp定义服务接口并提供出去jar被其他实现工程依赖bitpt与miz-pt依赖biz-pp的jar并实现SPI中的方法bitpt与miz-pt按照API规范实现完成后打成jar包或者安装到仓库中biz-pp在pom中依赖bitpt与miz-pt的jar或者通过启动加载的方式即可得到具体某个实现4.2 biz-pp 关键代码实现过程4.2.1 添加服务接口public interface MessagePlugin { public String sendMsg(Map msgMap); }4.2.2 打成jar包并安装到仓库这一步比较简单就不展开了4.2.3 自定义服务加载工具类这个类可以理解为在真实的业务编码中可以根据业务定义的规则具体加载哪个插件的实现类进行发送短信的操作public class PluginFactory { public void installPlugin(){ Map context new LinkedHashMap(); context.put(_userId,); context.put(_version,1.0); context.put(_type,sms); ServiceLoaderMessagePlugin serviceLoader ServiceLoader.load(MessagePlugin.class); IteratorMessagePlugin iterator serviceLoader.iterator(); while (iterator.hasNext()){ MessagePlugin messagePlugin iterator.next(); messagePlugin.sendMsg(context); } } public static MessagePlugin getTargetPlugin(String type){ ServiceLoaderMessagePlugin serviceLoader ServiceLoader.load(MessagePlugin.class); IteratorMessagePlugin iterator serviceLoader.iterator(); ListMessagePlugin 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) { casealiyun: if (messagePlugin instanceof BitptImpl){ targetPlugin messagePlugin; findTarget true; break; } casetencent: if (messagePlugin instanceof MizptImpl){ targetPlugin messagePlugin; findTarget true; break; } } if(findTarget) break; } return targetPlugin; } public static void main(String[] args) { new PluginFactory().installPlugin(); } }4.2.4 自定义接口RestController public class SmsController { Autowired private SmsService smsService; Autowired private ServiceLoaderUtils serviceLoaderUtils; //localhost:8087/sendMsg?msgsendMsg GetMapping(/sendMsg) public String sendMessage(String msg){ return smsService.sendMsg(msg); } }4.2.5 接口实现Service public class SmsService { Value(${msg.type}) private String msgType; Autowired private 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); } }4.2.6 添加服务依赖在该模块中需要引入对具体实现的两个工程的jar依赖也可以通过启动加载的命令方式dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !--依赖具体的实现-- dependency groupIdcom.congge/groupId artifactIdbiz-pt/artifactId version1.0-SNAPSHOT/version /dependency dependency groupIdcom.congge/groupId artifactIdmiz-pt/artifactId version1.0-SNAPSHOT/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency /dependenciesbiz-pp的核心代码实现就到此结束了后面再具体测试的时候再继续4.3 bizpt 关键代码实现过程接下来就是插件化机制中具体的SPI实现过程两个模块的实现步骤完全一致挑选其中一个说明工程目录结构如下4.3.1 添加对biz-app的jar的依赖将上面biz-app工程打出来的jar依赖过来dependencies dependency groupIdcom.congge/groupId artifactIdbiz-app/artifactId version1.0-SNAPSHOT/version /dependency /dependencies4.3.2 添加MessagePlugin接口的实现public class BitptImpl implements MessagePlugin { Override public 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; } }4.3.3 添加SPI配置文件按照前文的方式在resources目录下创建一个文件注意文件名称为SPI中的接口全名文件内容为实现类的全类名com.congge.spi.BitptImpl4.3.4 将jar安装到仓库中完成实现类的编码后通过maven命令将jar安装到仓库中然后再在上一步的biz-app中引入即可4.4 效果演示启动biz-app服务调用接口localhost:8087/sendMsg?msgsendMsg可以看到如下效果为什么会出现这个效果呢因为我们在实现类配置了具体使用哪一种方式进行短信的发送而加载插件的时候正好能够找到对应的服务实现这样的话就给当前的业务提供了一个较好的扩展点。