背景与问题
在追求效率和成本效益的背景下,软件复用成为了一种主流的做法,通过提供标准版本的产品或组件,提供了稳定、可靠且经过验证的功能。但对于那些有特定需求或环境的项目而言,标准版本可能无法完全满足他们的要求,这就引发了定制版本的需求。然而,过多的定制可能导致产品的标准化程度降低,从而增加了维护成本和复杂性。但是若一味遵循标准版本,可能无法满足特定项目的独特需求。因此,如何平衡标准版本与定制版本之间的关系,成为了一个关键的问题。
要解决这个问题,关键在于提高产品的可配置性和可扩展性,提供一套清晰、开放的定制化接口开发体系。这样,在保持产品标准版本稳定性的同时,也能满足特定项目的定制化需求。实现软件复用与定制化之间平衡。
一个简单的案例
以某项目短信模块为例,各个地区的短信服务商以及其提供的相关接口往往并不相同。这就带来了短信接口定制化的需求。
在过去很长的一段时间内,我们只能通过源码交付的方式,让项目开发团队去自行修改相关的代码,然后重新打包部署。但这会带来几个问题:
-
需要付出一定的代价让项目开发团队理解“源码”。
-
修改后的代码会形成一个特定的地区分支,不便于代码仓库的集中管理。
-
当标准产品代码发生变动和升级,往往会要求地区分支进行反复的合并工作。
这就导致软件复用与定制化之间发生了冲突,基于上述问题,我们引入了SPI机制。
解决问题的具体措施
我们的处理对策
以短信中心为例,我们抽象了一个发送短信的接口,主要解决短信发给谁?以及发送内容的问题。
public interface SmsService<T> {
/**
* 发送短信
* @param to 接收人
* @param content 短信内容
* @param params params
* @return String String
*/
String sendMessage(String to, String content, T params);
String getType();
}
标准产品中所有涉及到短信部分的业务逻辑,全部通过该接口来进行操作,如:
@Resource
SmsService smsService;
......
......
String result = smsService.sendMessage(mobile, contentText, null);
......
接口的实现逻辑如何填充(自定义短信发送插件)
首先,自定义一个maven工程。
<groupId>cn.xxx</groupId>
<artifactId>psf-sms-submail</artifactId>
<version>2.0.0-SNAPSHOP</version>
引入我们的短信接口定义包,如
<dependency>
<groupId>cn.xxx</groupId>
<artifactId>mcs-api</artifactId>
<version>${urbitech.essentia.version}</version>
</dependency>
实现已定义的接口
public class SubMailSmsServiceImpl implements SmsService {
@Override
public String sendMessage(String to, String content, Object params) {
//自定义短信发送的逻辑,如调用厂商短信的api接口
}
@Override
public String getType() {
return "短信厂商的标识";
}
}
在resource目录下,新建 META-INF/spring.factories)文件,将相关实现类纳入到spring的容器管理。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.submail.service.impl.SubMailSmsServiceImpl
如何使用上述插件
-
常规做法(不推荐)
修改主项目中pom.xml文件添加插件模块的依赖,然后重新编译打包。
-
通过启动项加载
将相关自定义插件单独打包,并放置在项目部署指定目录下,通过-Dloader.path 参数加载,参考
-Dloader.path用于指定额外的类加载路径。能够在无代码侵入的前提下修改相关的项目依赖。这一做法在之前nacos自定义数据源的插件中也有提及,可参考nacos的启动脚本。此外这种方式也用于解决springboot fat jar的问题。
当然有时候我们希望能够由产品提供默认实现,并在必要的情况下,加载自定义插件,此时我们可以手动实现一个类加载器,通过配置文件指定需要加载的Bean。如下图所示,其他使用方式与上文保持一致。
#业务驱动
service-driver:
speech-service: xxx.service.impl.WySpeechServiceImpl
@Configuration
public class ZwdmxConfig {
/**
* 指定驱动类名
*/
@Value("${zwdmx.service-driver.speech-service}")
private String speechServiceClassName;
@Bean
public SpeechService speechService() throws ClassNotFoundException {
Class<?> speechServiceClass = Class.forName(speechServiceClassName);
return (SpeechService) SpringUtil.getBean(speechServiceClass);
}
}