一、今天学习知识点
- 学习插件机制、了解nacos的插件机制
二、spring插件机制
在设计原则中有一个原则:依赖倒转原则,应该面向接口编程,也就是说应该依赖接口而不是依赖具体的实现,依赖接口,接口具有不同类型的实现,因此可以提高系统的扩展性,如果仅仅是依赖实现,那么就无法进行扩展了。
插件机制也就是一种可插拔的组件,也就是我们产品A需要用到功能B,这个功能B是一个接口,它的实现可以由我们引入外部jar来定义,或者在产品中实现多种实现。
2.1 spi机制
SPI全称(Service Provider interface),是JDK内置的一种服务提供发现机制,也就是一种插件机制。
其中工作原理,我们在工程中定义了一个接口A,在启动的时候会去CLASSPATH路径下的META-INF/services中,找到对应接口全路径下的文件文件,该文件内容是返回了一个实现类的全路径名称,这样就会将接口A的实现类给加载到工程中。
spi机制可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现具体的。
2.1.1 数据库驱动案例
我们程序引入的mysql,必然会引入一个mysql-connector-java
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
这里打开对应的jar就存在一个文件
这里的文件名 java.sql.Driver 其实就是一个全路径接口
这里的com.mysql.cj.jdbc.Driver就是该接口的一个实现类。
在DriverManager类中会去初始化对应Drivers
这里可以使用ServiceLoader调用对应接口实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
那么我们也可以使用对应的ServiceLoader.load获得我们接口的实现类。
底层使用classLoader通过fullName来获得对应的资源信息。
获得了对应的全路径名称,调用CLass.forName来加载对应class文件
class.forName可以在运行时动态加载对应类加载器下的资源class文件,这里的loader是属于应用类加载器,常见的类加载器有根类加载、扩展类加载、应用类加载器;
应用类加载器可以加载我们的jar下的文件资源,因此class.forName可以加载对应class文件。
然后通过反射获得对应对象
S p = service.cast(c.newInstance());
2.3 SPI缺点
- SPI机制会全部加载到内存,不会按需加载。
- 多个线程操作ClassLoader会导致并发问题(待调研)
2.2 springboot自动配置
我们springboot也提供类型SPI机制的插件形式,springboot提供自动配置的功能,如果在系统中引入了对应start的jar包,那么该jar的信息会被自动加载到spring bean中。
springboot在启动的时候也会去加载类加载下的 spring.factories文件,然后会立马内容,解析自动配置的实现类。
比如mybatis的自动配置是org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
该配置提供了诸多注解来自定义加载对应的bean到spring 容器中,因此会被spi机制更加高效,而是更加适应spring开发环境
三、nacos插件机制
在nacos中,提供了诸多插件机制,那么我们需要清楚这些插件是如何实现的,并且是否具备默认实现,以及我们使用了nacos我们如何定义我们的插件提供扩展的能力呢?
Nacos 1.2 版本之后加入了简易的账号密码体系,提供给社区进行简单的请求鉴权。如上图所示,客户端通过gRPC 或 HTTP 发送请求到服务端。用户开启鉴权功能后,在发送请求前,需要在请求中设置如用户名 、密码或生成的临时 token ,用于标记请求的身份信息。服务端从请求获取到对应的身份信息后,先进行身份验证,比如用户名、密码或 token 验证等,通过之后再进行权限校验,比如是否有读权限或写权限,全部通过后才会进行请求的正式处理。
因此很多用户都有升级的诉求,比如很多公司有自己的员工账号体系或安全系统, 需要对 Nacos 源代码进行修改后才能接入他们的安全系统。而 Nacos 社区处于高速发展期,代码变更非常频繁,对于广大用户或运维、开发者而言,维护成本非常高,需要频繁合并社区代码,并且需要解决过程中遇到的冲突问题。
在 2.1 版本添加鉴权插件之后
3.1 如何实现?
详见:nacos官方文档
其实大致流程也是和SPI流程一致,它就是使用SPI机制来注入的,首先NACOS会提供一个接口POM,引入对应的pom,然后实现它,在META-INF/service ,实现该接口文件,返回为对应的实现对象。
然后把该项目打包成jar放置在nacos的classLoader路径下,或者在xx/plugins下,然后nacos启动的时候,会自动加载ServiceLoader.loader加载对应的实现类,并且反射成对应的对象。
在nacos中,有一个配置文件
### 所启用的Nacos的鉴权插件的名称,与`com.alibaba.nacos.plugin.auth.spi.server.AuthPluginService`的`getAuthServiceName`返回值对应
nacos.core.auth.system.type=${authServiceName}
### 开启鉴权功能
nacos.core.auth.enabled=true
nacos.core.auth.system.type就是指定对应的插件,在数据库驱动插件中我们也要声明我们使用的插件:
这样即使注入多个插件,也能找到我们需要的插件机制,这个就是策略模式了。
当然如果我们需要开启nacos的config认证机制也可以使用nacos自带的配置。
四、Spring-plugin策略模式
如果有多个实现类,我们需要根据特定的type来找到我们的实现类,其实有诸多多方法。
-
使用map,key就是对应的type,value就是实现类
-
使用factoryBean,type为对应的beanName
-
编写工厂类,在接口层实现一个support方法,然后传入对应type,循环遍历所有实现类,看看哪个返回true,则为对应实现类。
这里在A接口中,query是执行业务逻辑,而belongToPullDataType则是对应的匹配这个是属于哪个实现类的。
定义一个工厂类,循环遍历所有的该类的实现类,然后带哦用belongToPullDataType,则可以返回一个合适的实现类。
4.1 引入Spring-plugin插件
其实spring早就考虑到了这种情况,因此也有一套spring-plugin插件,其实内置使用也和上述类似,只不过它的register提供了更多的方法,比如找不到实现类、抛出异常,或者返回默认实现类等等。但是这个必须你得使用spring框架。
- 引入依赖
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
- 定义接口,并且继承Plugin<Type> ,这段type就是需要传入什么对象来区分你要使用的哪个实现类
import org.springframework.plugin.core.Plugin;
/**
* 短信插件
*/
public interface SmsPlugin extends Plugin<SmsType> {
/**
* 发送短信
*
* @param phone 手机号
* @param content 短信内容
*/
void sendSms(String phone, String content);
}
- 接口实现
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(2)
public class AliYunSmsPluginProvider implements SmsPlugin {
private static final Logger log = LoggerFactory.getLogger(AliYunSmsPluginProvider.class);
// 这个就是有区分的
@Override
public boolean supports(SmsType smsType) {
return smsType == SmsType.A_LI_YUN;
}
@Override
public void sendSms(String phone, String content) {
log.info("通过阿里云渠道 给phone:[{}]发送短信:[{}]成功", phone, content);
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class TenXunYunSmsPluginProvider implements SmsPlugin {
private static final Logger log = LoggerFactory.getLogger(TenXunYunSmsPluginProvider.class);
@Override
public boolean supports(SmsType smsType) {
return smsType == SmsType.TX_YUN;
}
@Override
public void sendSms(String phone, String content) {
log.info("通过腾讯云渠道 给phone:[{}]发送短信:[{}]成功", phone, content);
}
}
- 添加配置
import com.jcl.plugin.sms.SmsPlugin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.plugin.core.config.EnablePluginRegistries;
@SpringBootApplication
@EnablePluginRegistries(value = {SmsPlugin.class})
public class SpringPluginApplication {
public static void main(String[] args) {
SpringApplication.run(SpringPluginApplication.class, args);
}
}
- 使用PluginRegistry来调用
@SpringBootTest
class SpringPluginApplicationTest {
@Autowired
private PluginRegistry<SmsPlugin, SmsType> pluginRegistry;
@Test
void test() {
SmsPlugin plugin = pluginRegistry.getRequiredPluginFor(SmsType.A_LI_YUN);
plugin.sendSms("1895****705", "测试");
}
}
五、dubbo的SPI机制
从我们分析java的spi机制可以看出,java约定了文件名为接口的名称,内容为实现。不知道大家有没有想过这里面有个很严重的问题,就是虽然我获取到了所有的实现类,但是无法对实现类进行分类,也就是说我无法确定到底该用哪个实现类,并且java的spi机制会一次性给所有的实现类创建对象,如果这个对象你根本不会使用,那么此时就会白白浪费资源,也就是说把所有的都加载了,无法做到按需加载。
例如com.alibaba.dubbo.rpc.Protocol接口有InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol等实现,如果只是用到其中一个实现,却要加载全部实现,会导致资源的浪费。
比如上述说到的数据库配置,如果我们加载了多个数据源,有多个驱动,那么我们肯定要知道我应该使用哪个驱动对吧。因此我们需要单独写一块代码去读配置,标注我们需要使用哪个实现类,nacos的nacos.core.auth.system.type也是指定对应的插件。
dubbo的spi机制就解决了这个问题。
dubbo把配置文件中扩展实现的格式修改为键值对格式,例如META-INF/dubbo/com.xxx.Protocol里的com.foo.XxxProtocol格式改为了xxx = com.foo.XxxProtocol这种以键值对的形式
在META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol文件中:
filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=org.apache.dubbo.rpc.support.MockProtocol
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=org.apache.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol
http=org.apache.dubbo.rpc.protocol.http.HttpProtocol
org.apache.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=org.apache.dubbo.rpc.protocol.thrift.ThriftProtocol
native-thrift=org.apache.dubbo.rpc.protocol.nativethrift.ThriftProtocol
memcached=org.apache.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=org.apache.dubbo.rpc.protocol.redis.RedisProtocol
rest=org.apache.dubbo.rpc.protocol.rest.RestProtocol
xmlrpc=org.apache.dubbo.xml.rpc.protocol.xmlrpc.XmlRpcProtocol
registry=org.apache.dubbo.registry.integration.RegistryProtocol
qos=org.apache.dubbo.qos.protocol.QosProtocolWrapper
这些都是dubbo的Protocol接口的实现类。
dubbo支持三个目录下的配置文件搜寻,其中文件名也是接口的全路径
"META-INF/dubbo/internal/"是dubbo内部提供的扩展的配置文件路径
"META-INF/services/"是dubbo为了兼容jdk的SPI扩展机制思想而设存在的
"META-INF/dubbo/"是为了给用户自定义的扩展实现配置文件存放
通过三种方式可以加载对应的实现类。
ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension(); // 读取默认的配置,@SPI上的value
ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name); // 读取对应key名称的name
ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);
首先定义一个扩展接口
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
}
这里使用了@SPI注解,里面的dubbo则是默认值,后续可以通过
ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();获得默认的实现类,也就是org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol。