SpringBoot 插件化开发模式
1、Java常用插件实现方案
1.2、serviceloader方式
serviceloader是java提供的spi模式的实现。按照接口开发实现类,而后配置,java通过ServiceLoader来实现统一接口不同实现的依次调用。而java中最经典的serviceloader的使用就是Java的spi机制。
1.2.1、java spi
SPI全称 Service Provider Interface ,是JDK内置的一种服务发现机制,SPI是一种动态替换扩展机制,比如有个接口,你想在运行时动态给他添加实现,你只需按照规范给他添加一个实现类即可。比如大家熟悉的jdbc中的Driver接口,不同的厂商可以提供不同的实现,有mysql的,也有oracle的,而Java的SPI机制就可以为某个接口寻找服务的实现。
下面用一张简图说明下SPI机制的原理
1.2.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");
return "aliyun sendMsg";
}
}
public class TencentMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("tencent sendMsg");
return "tencent sendMsg";
}
}
在resources目录按照规范要求创建文件目录(META-INF/services),文件名为接口的全限定名,并填写实现类的全限定类名。
自定义服务加载类
public static void main(String[] args) {
ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
Iterator<MessagePlugin> iterator = serviceLoader.iterator();
Map map = new HashMap();
while (iterator.hasNext()){
MessagePlugin messagePlugin = iterator.next();
messagePlugin.sendMsg(map);
}
}
运行上面的程序后,可以看到下面的效果,这就是说,使用ServiceLoader的方式可以加载到不同接口的实现,业务中只需要根据自身的需求,结合配置参数的方式就可以灵活的控制具体使用哪一个实现。
1.2、自定义配置约定方式
serviceloader其实是有缺陷的,在使用中必须在META-INF里定义接口名称的文件,在文件中才能写上实现类的类名,如果一个项目里插件化的东西比较多,那很可能会出现越来越多配置文件的情况。所以在结合实际项目使用时,可以考虑下面这种实现思路:
- A应用定义接口;
- B,C,D等其他应用定义服务实现;
- B,C,D应用实现后达成SDK的jar;
- A应用引用SDK或者将SDK放到某个可以读取到的目录下;
- A应用读取并解析SDK中的实现类;
在上文中案例基础上,我们做如下调整;
1.2.1、添加配置文件
在配置文件中,将具体的实现类配置进去
server:
port: 8888
impl:
name: com.wq.plugins.spi.MessagePlugin
clazz:
- com.wq.plugins.impl.AliyunMsg
- com.wq.plugins.impl.TencentMsg
1.2.2、自定义配置文件加载类
通过这个类,将上述配置文件中的实现类封装到类对象中,方便后续使用;
package com.wq.propertie;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Arrays;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/1
* @Author wandaren
*/
// 启动类需要添加@EnableConfigurationProperties({ClassImpl.class})
@ConfigurationProperties("impl")
public class ClassImpl {
private String name;
private String[] clazz;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String[] getClazz() {
return clazz;
}
public void setClazz(String[] clazz) {
this.clazz = clazz;
}
public ClassImpl(String name, String[] clazz) {
this.name = name;
this.clazz = clazz;
}
public ClassImpl() {
}
@Override
public String toString() {
return "ClassImpl{" +
"name='" + name + '\'' +
", clazz=" + Arrays.toString(clazz) +
'}';
}
}
1.2.3、自定义测试接口
使用上述的封装对象通过类加载的方式动态的在程序中引入
package com.wq.contorller;
import com.wq.plugins.spi.MessagePlugin;
import com.wq.propertie.ClassImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/1
* @Author wandaren
*/
@RestController
public class HelloController {
@Autowired
private ClassImpl 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";
}
}
1.2.4、启动类
package com.wq;
import com.wq.propertie.ClassImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
启动工程代码后,调用接口:localhost:8888/sendMsg,在控制台中可以看到下面的输出信息,即通过这种方式也可以实现类似serviceloader的方式,不过在实际使用时,可以结合配置参数进行灵活的控制;
1.3、自定义配置读取依赖jar的方式
更进一步,在很多场景下,可能我们并不想直接在工程中引入接口实现的依赖包,这时候可以考虑通过读取指定目录下的依赖jar的方式,利用反射的方式进行动态加载,这也是生产中一种比较常用的实践经验。
具体实践来说,主要为下面的步骤:
- 应用A定义服务接口(安装到maven);
- 应用B,C,D等实现接口(或者在应用内部实现相同的接口),dependency引用服务A;
- 应用B,C,D打成jar,放到应用A约定的读取目录下;
- 应用A加载约定目录下的jar,通过反射加载目标方法;
在上述的基础上,按照上面的实现思路来实现一下;
- 应用A定义接口
public interface MessagePlugin {
public String sendMsg(Map msgMap);
}
安装到本地maven仓库
- 应用B,C实现接口
<dependency>
<groupId>com.wq</groupId>
<artifactId>spi-00</artifactId>
<version>1</version>
</dependency>
- 应用B
public class AliyunMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("aliyun sendMsg");
return "aliyun sendMsg";
}
}
- 应用C
public class TencentMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("tencent sendMsg");
return "tencent sendMsg";
}
}
- 将应用B、C打成jar
在工程下创建一个lib目录,并将依赖的jar放进去
1.3.2、新增读取jar的工具类
添加一个工具类,用于读取指定目录下的jar,并通过反射的方式,结合配置文件中的约定配置进行反射方法的执行;
package com.wq.utils;
import com.wq.propertie.ClassImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Component
public class ServiceLoaderUtils {
@Autowired
ClassImpl classImpl;
public static void loadJarsFromAppFolder() throws Exception {
String path = "/Users/wandaren/develop/study/spi-00/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 = "/Users/wandaren/develop/study/spi-00/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;
//执行反射相关的方法
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("spi-01-1.jar")){
if(!claName.equals("com.wq.plugins.impl.AliyunMsg")){
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("spi-02-1.jar")){
if(!claName.equals("com.wq.plugins.impl.TencentMsg")){
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 = "/Users/wandaren/develop/study/spi-00/lib/spi-01-1.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.wq.plugins.impl.AliyunMsg");
//获取实例
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();
}
}
1.3.3、添加测试接口
@Autowired
private ServiceLoaderUtils serviceLoaderUtils;
@GetMapping("/sendMsgV2")
public String index() throws Exception {
String result = serviceLoaderUtils.doExecuteMethod();
return result;
}
以上全部完成之后,启动工程,测试一下该接口,仍然可以得到预期结果;
在上述的实现中还比较粗糙的,实际运用时,还需要做较多的优化改进以满足实际的业务需要,比如接口传入类型参数用于控制具体使用哪个依赖包的方法进行执行等;
2、SpringBoot中的插件化实现
在大家使用较多的springboot框架中,其实框架自身提供了非常多的扩展点,其中最适合做插件扩展的莫过于spring.factories的实现;
2.1、 Spring Boot中的SPI机制
在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化,这种自定义的SPI机制是Spring Boot Starter实现的基础。
2.2、 Spring Factories实现原理
spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
- loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表;
- loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表;
上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
从代码中我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件,就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。
spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:
com.xxx.interface=com.xxx.classname
如果一个接口希望配置多个实现类,可以使用’,’进行分割
2.3、Spring Factories案例实现
接下来看一个具体的案例实现来体验下Spring Factories的使用;
2.3.1、定义一个服务接口
自定义一个接口,里面添加一个方法;
public interface SmsPlugin {
public void sendMessage(String message);
}
2.3.2、 定义2个服务实现
实现类1
package com.wq.plugin.impl;
import com.wq.plugin.SmsPlugin;
public class BizSmsImpl implements SmsPlugin {
@Override
public void sendMessage(String message) {
System.out.println("this is BizSmsImpl sendMessage..." + message);
}
}
实现类2
package com.wq.plugin.impl;
import com.wq.plugin.SmsPlugin;
public class SystemSmsImpl implements SmsPlugin {
@Override
public void sendMessage(String message) {
System.out.println("this is SystemSmsImpl sendMessage..." + message);
}
}
2.3.3、 添加spring.factories文件
在resources目录下,创建一个名叫:META-INF的目录,然后在该目录下定义一个spring.factories的配置文件,内容如下,其实就是配置了服务接口,以及两个实现类的全类名的路径;
com.wq.plugin.SmsPlugin=\
com.wq.plugin.impl.BizSmsImpl,\
com.wq.plugin.impl.SystemSmsImpl
2.3.4、 添加自定义接口
添加一个自定义的接口,有没有发现,这里和java 的spi有点类似,只不过是这里换成了SpringFactoriesLoader去加载服务;
package com.wq.controller;
import com.wq.plugin.SmsPlugin;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/2
* @Author wandaren
*/
@RestController
public class SmsController {
@GetMapping("/sendMsgV3")
public String sendMsgV3(String msg) throws Exception{
List<SmsPlugin> smsServices= SpringFactoriesLoader.loadFactories(SmsPlugin.class, null);
for(SmsPlugin smsService : smsServices){
smsService.sendMessage(msg);
}
return "success";
}
}
启动工程之后,调用一下该接口进行测试,localhost:8080/sendMsgV3?msg=hello,通过控制台,可以看到,这种方式能够正确获取到系统中可用的服务实现;
利用spring的这种机制,可以很好的对系统中的某些业务逻辑通过插件化接口的方式进行扩展实现;
3、插件化机制案例实战
结合上面掌握的理论知识,下面基于Java SPI机制进行一个接近真实使用场景的完整的操作步骤;
3.1、 案例背景
- 3个微服务模块,在A模块中有个插件化的接口;
- 在A模块中的某个接口,需要调用插件化的服务实现进行短信发送;
- 可以通过配置文件配置参数指定具体的哪一种方式发送短信;
- 如果没有加载到任何插件,将走A模块在默认的发短信实现;
3.1.1、 模块结构
1、spi-00,插件化接口工程;
2、spi-01,aliyun短信发送实现;
3、spi-02,tncent短信发送实现;
3.1.2、 整体实现思路
本案例完整的实现思路参考如下:
- spi-00定义服务接口,并提供出去jar被其他实现工程依赖;
- spi-01与spi-02依赖spi-00的jar并实现SPI中的方法;
- spi-01与spi-02按照API规范实现完成后,打成jar包,或者安装到仓库中;
- spi-00在pom中依赖spi-01与的jar,spi-02或者通过启动加载的方式即可得到具体某个实现;
3.2、spi-00添加服务接口
3.2.1、 添加服务接口
public interface MessagePlugin {
public String sendMsg(Map msgMap);
}
3.2.2、 打成jar包并安装到仓库
idea执行install
3.3、spi-01与spi-02实现
maven引入spi-00依赖坐标
<dependencies>
<dependency>
<groupId>com.wq</groupId>
<artifactId>spi-00</artifactId>
<version>1</version>
</dependency>
</dependencies>
3.3.1、spi-01
public class AliyunMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("aliyun sendMsg");
return "aliyun sendMsg";
}
}
3.3.2、spi-02
public class TencentMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("tencent sendMsg");
return "tencent sendMsg";
}
}
3.3.3、将spi-01与spi-02打成jar
idea执行install
3.4、spi-00添加服务依赖与实现
3.4.1、添加服务依赖
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring-boot.version>2.7.4</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.wq</groupId>
<artifactId>spi-01</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>com.wq</groupId>
<artifactId>spi-02</artifactId>
<version>1</version>
</dependency>
</dependencies>
3.4.2、自定义服务加载工具类
package com.wq.spi;
import java.util.*;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/2
* @Author wandaren
*/
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 AliyunMsg){
targetPlugin = messagePlugin;
findTarget = true;
break;
}
case "tencent":
if (messagePlugin instanceof TencentMsg){
targetPlugin = messagePlugin;
findTarget = true;
break;
}
default: break;
}
if(findTarget) {
break;
}
}
return targetPlugin;
}
public static void main(String[] args) {
new PluginFactory().installPlugin();
}
}
3.4.3、接口实现
package com.wq.service;
import com.wq.spi.MessagePlugin;
import com.wq.spi.PluginFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@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);
}
}
package com.wq.service;
import com.wq.spi.MessagePlugin;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/2
* @Author wandaren
*/
@Service
public class DefaultSmsService implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
return "DefaultSmsService--------";
}
}
3.4.4、测试controller
package com.wq.controller;
import com.wq.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SmsController {
@Autowired
private SmsService smsService;
@GetMapping("/sendMsg")
public String sendMessage(String msg){
return smsService.sendMsg(msg);
}
}
3.4.5、测试
通过修改配置application.yml中msg.type的值切换不同实现
msg:
# type: tencent
type: aliyun