掘金 后端 ( ) • 2024-06-25 09:54

theme: fancy

前置知识

参考链接:https://www.cnblogs.com/zwh0910/p/15459737.html

常见的ConditionalOnxxx开头的注解,我们称之为条件注解,用于控制当前bean是否被加载,如果不满足条件,则不加载当前bean

  • ConditionalOnProperty:
  • ConditionalOnResource
  • ConditionalOnBean
  • ConditionalOnClass
  • ConditionalOnMissingBean
  • ConditionalOnMissingClass

SPI机制

https://javaguide.cn/java/basis/spi.html#spi-和-api-有什么区别
https://pdai.tech/md/java/advanced/java-advanced-spi.html

SPI全称是Service Provider Interface,中文名为服务提供者接口,基于JDK内置的动态加载实现扩展点的机制(通过在类路径下的META-INF/services文件夹中查找文件,自动加载文件里所定义的类),这一机制使得框架扩展和替换组件更加方便。常见的使用SPI机制的场景有:日志接口log4j、数据库驱动(不同的数据库厂商实现定义的接口)以及Dubbo的扩展实现。 image.png

  • 当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
  • 当服务的提供者实现接口后,需要在类路径下的META-INF/services目录(这个目录也可以自定义,在shenyu项目中就是META-INF/shenyu)里创建一个以服务接口命名的文件,文件内容就是这个接口的具体实现类路径。当其他程序需要这个服务时,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services中的配置文件,根据具体实现类的类名进行实例化加载。JDK中查找接口的实现类工具是:java.util.ServiceLoader。

Shenyu-nginx模块实现集群

该模块提供SDK,用于通过注册中心为OpenResty自动监听Apache Shenyu可用的网关实例节点。 在集群模式下,Apache Shenyu支持部署多个Shenyu实例,随时可能有新的实例上线或下线。因此,Apache Shenyu引入了服务发现 OpenResty 模块来帮助客户端检测可用Shenyu实例。目前Apache Shenyu已经支持Zookeeper、Nacos、Etcd和Consul。Client或LoadBalancer 可以通过这些Service注册中心获取可用的Shenyu实例。

首先,下载源码

git clone https://github.com/apache/shenyu-nginx

然后,构建源码并安装

cd shenyu-nginx luarocks make rockspec/shenyu-nginx-main-0.rockspec

接着,修改Nginx配置,创建并初始化Shenyu register模块,连接至目标注册中心。该模块将获取在同一个集群中注册到Etcd的 所有Shenyu实例。它与Etcd客户端一样监视(基于长轮询)Shenyu实例列表。 Etcd示例:

init_worker_by_lua_block { 
     local register = require("shenyu.register.etcd") 
     register.init({ balancer_type = "chash", etcd_base_url = "http://127.0.0.1:2379", }) 
}

最后,重启OpenResty

openresty -s reload

Bootstrap实例如何注册

注册中心初始化

Bootstrap启动后如何向admin注册实例信息,从Bootstrap的pom文件中发现如下依赖:

<dependency>
    <groupId>org.apache.shenyu</groupId>
    <artifactId>shenyu-spring-boot-starter-registry</artifactId>
    <version>${project.version}</version>
</dependency>
# 除了这块依赖应该还需引入相应注册中心的依赖

org.apache.shenyu.springboot.starter.registry.ShenyuRegistryConfiguration,这个类是一个配置类,初始化了RegisterConfig和RegistryListener。RegistryListener实现了ApplicationListener:

public RegistryListener(final RegisterConfig config) {
    String registerType = config.getRegisterType();
    String serverLists = config.getServerLists();
    if (StringUtils.isBlank(registerType) || StringUtils.isBlank(serverLists)) {
        throw new ShenyuException("please config the registerType and serverList");
    }
    // 读取注册中心配置文件,所以注册中心用于注册网关实例、URI、元数据等
    // 刚开始我以为网关实例注册和元数据注册是分别不同的注册配置
    repository = ShenyuInstanceRegisterRepositoryFactory.newAndInitInstance(config);
    this.props = config.getProps();
    String name = props.getProperty("name");
    this.appName = StringUtils.isBlank(name) ? "shenyu-gateway" : name;
    String host = props.getProperty("host");
    // 获取网关实例的ip
    this.host = StringUtils.isBlank(host) ? IpUtils.getHost() : host;
}
  • 从初始化逻辑中可以看懂会根据注册中心配置,利用工厂模式创建一个ShenyuInstanceRegisterRepository。 ShenyuInstanceRegisterRepositoryFactory,可以发现使用SPI机制,加载ShenyuInstanceRegisterRepository所有的实现类,通过map存储,newAndInitInstance方法则根据注册类型从map中获取相应的网关实例注册类。
  • 工厂模式:ShenyuInstanceRegisterRepositoryFactory
  • 策略模式:ShenyuInstanceRegisterRepository作为顶层接口,子类实现这个接口,利用SPI将所有实现类注册至map,根据注册类型不同,获取相应的实现类执行具体逻辑;这种模式很常见,也很好用。
  • REPOSITORY_MAP是线程安全的map,这里考虑了线程安全问题
  • result.init:注册中心的初始化
public final class ShenyuInstanceRegisterRepositoryFactory {
    
    private static final Map<String, ShenyuInstanceRegisterRepository> REPOSITORY_MAP = new ConcurrentHashMap<>();
    
    public static ShenyuInstanceRegisterRepository newInstance(final String registerType) {
        return REPOSITORY_MAP.computeIfAbsent(registerType, ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class)::getJoin);
    }
    
    public static ShenyuInstanceRegisterRepository newAndInitInstance(final RegisterConfig config) {
        return REPOSITORY_MAP.computeIfAbsent(config.getRegisterType(), registerType -> {
            ShenyuInstanceRegisterRepository result = ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin(registerType);
            result.init(config);
            return result;
        });
    }
}

网关实例注册至注册中心

org.apache.shenyu.springboot.starter.registry.RegistryListener

public void onApplicationEvent(final WebServerInitializedEvent event) {
    if (!registered.compareAndSet(false, true)) {
        return;
    }
    String configPort = props.getProperty("port");
    int port = StringUtils.isBlank(configPort) ? event.getWebServer().getPort() : Integer.parseInt(configPort);
    InstanceEntity instanceEntity = buildInstanceRegisterDTO(port);
    repository.persistInstance(instanceEntity);
}
  • WebServerInitializedEvent:tomcat启动之后,会发布ServletWebServerInitializedEvent事件,该类是WebServerInitializedEvent的子类。也就是说,当tomcat启动后,才注册网关实例至注册中心。
  • registered是AtomicBoolean,线程安全,用于判断是否已经注册。

ShenyuInstanceRegisterRepository从字面意思理解是用于注册网关实例的类,有若干的实现类,实现类逻辑在shenyu-registry模块中:

image.png 以nacos为例,org.apache.shenyu.registry.nacos.NacosInstanceRegisterRepository#persistInstance逻辑如下:

@Override
public void persistInstance(final InstanceEntity instance) {
    try {
        Instance inst = new Instance();
        inst.setWeight(1.0d);
        inst.setEphemeral(true);
        inst.setIp(instance.getHost());
        inst.setPort(instance.getPort());
        inst.setInstanceId(buildInstanceNodeName(instance));
        inst.setServiceName(instance.getAppName());
        namingService.registerInstance(instance.getAppName(), groupName, inst);
        LOGGER.info("nacos client register success: {}", inst);
    } catch (NacosException e) {
        throw new ShenyuException(e);
    }
}
  • 使用nacos的namingService注册该实例信息