【每天学习一点点】Java SPI机制、dubbo spi机制、Springboot自动配置、nacos插件机制以及使用Spring-plugin策略模式

文章介绍了Java的SPI服务提供者发现机制,包括在数据库驱动和SpringBoot自动配置中的应用。接着讲解了Nacos的插件机制,如何通过SPI实现鉴权插件,并提及SpringPlugin策略模式来管理多个实现类。最后对比了Dubbo的SPI机制,强调其在资源管理和按需加载方面的优化。
摘要由CSDN通过智能技术生成

一、今天学习知识点

  1. 学习插件机制、了解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缺点

  1. SPI机制会全部加载到内存,不会按需加载。
  2. 多个线程操作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来找到我们的实现类,其实有诸多多方法。

  1. 使用map,key就是对应的type,value就是实现类

  2. 使用factoryBean,type为对应的beanName

  3. 编写工厂类,在接口层实现一个support方法,然后传入对应type,循环遍历所有实现类,看看哪个返回true,则为对应实现类。

这里在A接口中,query是执行业务逻辑,而belongToPullDataType则是对应的匹配这个是属于哪个实现类的。
在这里插入图片描述
定义一个工厂类,循环遍历所有的该类的实现类,然后带哦用belongToPullDataType,则可以返回一个合适的实现类。
在这里插入图片描述

4.1 引入Spring-plugin插件

其实spring早就考虑到了这种情况,因此也有一套spring-plugin插件,其实内置使用也和上述类似,只不过它的register提供了更多的方法,比如找不到实现类、抛出异常,或者返回默认实现类等等。但是这个必须你得使用spring框架。

  1. 引入依赖
<dependency>
	<groupId>org.springframework.plugin</groupId>
	<artifactId>spring-plugin-core</artifactId>
	<version>2.0.0.RELEASE</version>
</dependency>
  1. 定义接口,并且继承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);
}

  1. 接口实现

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);
    }
}

  1. 添加配置
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);
    }

}

  1. 使用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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值