基于Java SPI机制实现可插拔的应用插件

1 概述

Java Service Provider Interface (SPI) 机制是 Java 提供的一种用于实现组件化、可插拔式架构的机制。通过 SPI,Java 应用程序可以在运行时动态地加载和实例化服务实现类,而无需在编译时将其硬编码到应用程序中。本文将介绍如何通过使用Java的Service Provider Interface (SPI) 机制实现可插拔的应用插件。SPI整体机制图如下:
在这里插入图片描述

2 使用场景

在实际项目中,常常会提出各种定制化需求。这些需求往往具有特定性,仅适用于当前项目或特定客户。为了满足这些需求,同时保持系统的灵活性和可维护性,我们可以考虑使用插件化架构开发定制化需求,与标版代码进行解耦。以下列举了一些使用场景:

2.1 登录认证

如项目需要支持多种身份验证方式,比如LDAP、OAuth、SAML等。通过插件化架构,可以根据配置动态加载不同的认证插件。

2.2 告警业务

告警服务是项目中不可或缺的一部分,不同的项目可能对告警方式有不同的要求。比如短信、邮件、企业微信、钉钉、飞书等。通过插件化架构,可以为每种告警方式开发独立的插件,并在运行时动态加载。

3 插件实现及使用

3.1 定义插件标准接口

插件标准接口需要定义在独立的模块中,并作为业务服务的子模块。插件具体实现模块需要引用该标准接口模块依赖。

3.1.1 定义标准接口

笔者以登录认证为示例,假设我们有一个聚合工程cs-auth,在cs-auth工程中创建标准的认证插件cs-auth-plugin模块,在cs-auth-plugin中定义标准接口AuthPluginService

/**
 * Auth service.
 */
public interface AuthPluginService {

    /**
     * To validate whether the username and password is legal or illegal.
     */
    boolean login(String userName, String password);

    /**
     * AuthPluginService Name which for conveniently find AuthPluginService instance.
     *
     * @return AuthServiceName mark a AuthPluginService instance.
     */
    String getAuthServiceName();

}

3.1.2 定义插件管理器

cs-auth-plugins定义插件管理器AuthPluginManager,用于动态加载和管理身份验证插件服务(AuthPluginService)的实现。

/**
 * Load Plugins.
 *
 */
public class AuthPluginManager {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthPluginManager.class);
    
    private static final AuthPluginManager INSTANCE = new AuthPluginManager();
    
    /**
     * The relationship of context type and {@link AuthPluginService}.
     */
    private final Map<String, AuthPluginService> authServiceMap = new HashMap<>();
    
    private AuthPluginManager() {
        initAuthServices();
    }
    
    private void initAuthServices() {
        Collection<AuthPluginService> authPluginServices = CsServiceLoader.load(AuthPluginService.class);
        for (AuthPluginService each : authPluginServices) {
            if (StringUtils.isBlank(each.getAuthServiceName())) {
                LOGGER.warn(
                        "[AuthPluginManager] Load AuthPluginService({}) AuthServiceName(null/empty) fail. Please Add AuthServiceName to resolve.",
                        each.getClass());
                continue;
            }
            authServiceMap.put(each.getAuthServiceName(), each);
            LOGGER.info("[AuthPluginManager] Load AuthPluginService({}) AuthServiceName({}) successfully.",
                    each.getClass(), each.getAuthServiceName());
        }
    }
    
    public static AuthPluginManager getInstance() {
        return INSTANCE;
    }
    
    /**
     * get AuthPluginService instance which AuthPluginService.getType() is type.
     *
     * @param authServiceName AuthServiceName, mark a AuthPluginService instance.
     * @return AuthPluginService instance.
     */
    public Optional<AuthPluginService> findAuthServiceSpiImpl(String authServiceName) {
        return Optional.ofNullable(authServiceMap.get(authServiceName));
    }
    
}

3.1.3 定义服务加载器

cs-auth-plugin中定义类CsServiceLoader, CsServiceLoader` 类是一个自定义的 SPI(Service Provider Interface)服务加载器,它通过 Java 反射机制实现服务的动态加载和实例化。

/**
 * CS SPI Service Loader.
 *
 */
public class CsServiceLoader {
    
    private static final Map<Class<?>, Collection<Class<?>>> SERVICES = new ConcurrentHashMap<>();
    
    /**
     * Load service.
     *
     * <p>Load service by SPI and cache the classes for reducing cost when load second time.
     *
     * @param service service class
     * @param <T> type of service
     * @return service instances
     */
    public static <T> Collection<T> load(final Class<T> service) {
        if (SERVICES.containsKey(service)) {
            return newServiceInstances(service);
        }
        Collection<T> result = new LinkedHashSet<>();
        for (T each : ServiceLoader.load(service)) {
            result.add(each);
            cacheServiceClass(service, each);
        }
        return result;
    }

    private static <T> void cacheServiceClass(final Class<T> service, final T instance) {
        SERVICES.computeIfAbsent(service, k -> new LinkedHashSet<>()).add(instance.getClass());
    }
    
    /**
     * New service instances.
     *
     * @param service service class
     * @param <T> type of service
     * @return service instances
     */
    public static <T> Collection<T> newServiceInstances(final Class<T> service) {
        return SERVICES.containsKey(service) ? newServiceInstancesFromCache(service) : Collections.<T>emptyList();
    }
    
    @SuppressWarnings("unchecked")
    private static <T> Collection<T> newServiceInstancesFromCache(Class<T> service) {
        Collection<T> result = new LinkedHashSet<>();
        for (Class<?> each : SERVICES.get(service)) {
            result.add((T) newServiceInstance(each));
        }
        return result;
    }
    
    private static Object newServiceInstance(final Class<?> clazz) {
        try {
            return clazz.getDeclaredConstructor().newInstance();
        }catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}
3.2 提供插件实现

一般情况下,会提供多个插件实现。插件的通用实现通常是为了满足大多数场景的需求,作为基础实现包含在业务服务工程中。而插件的定制化实现则针对特定项目的特殊需求,创建独立的工程,在部署时动态加载该插件,实现与业务服务的解耦。

3.2.1 通用插件实现

3.2.1.1 引入标准插件依赖

在聚合工程cs-auth里面创建业务服务cs-server-auth, 在业务服务cs-server-auth中引入步骤1中定义的标准插件依赖

 <dependency>
      <groupId>com.cs</groupId>
      <artifactId>cs-auth-plugin</artifactId>
      <version>1.0.0-SNAPSHOT</version>
  </dependency>

3.2.1.2 定义插件通用实现

cs-server-auth的中定义 AuthPluginService 接口的实现:

public class DafaultAuthService implements AuthPluginService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DafaultAuthService.class);

    @Override
    public boolean login(String username, String password) {
        LOGGER.info("用户名密码鉴权成功了");
        return true;
    }

    @Override
    public String getAuthServiceName() {
        return "default";
    }
}

2.1.3 配置SPI文件

在资源目录 src/main/resources 中创建一个名为 META-INF/services 的目录。然后在这个目录下创建一个文件,文件名为插件接口的全限定名。例如,对于 AuthPluginService 接口,文件名应该是 META-INF/services/com.cs.auth.plugin.spi.AuthPluginService

在文件 META-INF/services/com.cs.auth.plugin.spi.AuthPluginService 中列出所有AuthPluginService` 接口的实现类的全限定名:

com.cs.auth.server.plugin.DafaultAuthService
3.2.2 定制化插件实现

3.2.2.1 引入标准插件依赖

在聚合工程cs-auth中创建工程cs-ldap-auth-plugin,并引入 步骤1中定义的标准插件依赖

 <dependency>
      <groupId>com.cs</groupId>
      <artifactId>cs-auth-plugin</artifactId>
      <version>1.0.0-SNAPSHOT</version>
  </dependency>

2.2.2 定义定制化插件实现
cs-ldap-auth-plugin模块中,定义实现 AuthPluginService 接口的类:

public class LdapAuthPluginService implements AuthPluginService {

    private static final Logger LOGGER = LoggerFactory.getLogger(LdapAuthPluginService.class);

    @Override
    public boolean login(String userName, String password) {
        LOGGER.info("LDAP认成功...");
        return true;
    }

    @Override
    public String getAuthServiceName() {
        return "ldap";
    }
}

3.2.2.3 配置SPI文件

在资源目录 src/main/resources 中创建一个名为 META-INF/services 的目录。然后在这个目录下创建一个文件,文件名为插件接口的全限定名。例如,对于 AuthPluginService 接口,文件名应该是 META-INF/services/com.cs.auth.plugin.spi.AuthPluginService
在文件 META-INF/services/com.cs.auth.plugin.spi.AuthPluginService 中列出所有AuthPluginService` 接口的实现类的全限定名:

com.cs.ldap.plugin.auth.impl.LdapAuthPluginService
3.3 动态加载插件

以下步骤3.1至3.4用于加载通用插件实现,步骤3.5用于加载定制化插件实现。如果业务服务只有通用插件实现,则只需执行步骤3.1至3.4。如果包含定制化插件的实现,则需要执行步骤3.1至3.5

3.3.1 选择插件实现

application.yml配置文件中选择步骤二中定义的某一种插件实现作为实际鉴权方式

cs:
  auth:
    system:
      type: default
3.3.2 使用插件
@Component
public class AuthService {


    @Value("${cs.auth.system.type}")
    private String csAuthSystemType;

    public boolean login(String username, String password) {
        Optional<AuthPluginService> authPluginService = AuthPluginManager.getInstance().findAuthServiceSpiImpl(csAuthSystemType);
        return authPluginService.map(pluginService -> pluginService.login(username, password)).orElse(false);
    }
}
3.3.3 POM打包配置

Spring Boot Maven打包插件配置**<layout>ZIP</layout>**

 <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>${spring-boot.version}</version>
    <configuration>
        <mainClass>com.cs.auth.server.BootstrapApplication</mainClass>
        <layout>ZIP</layout>
    </configuration>
    <executions>
        <execution>
            <id>repackage</id>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
</plugin>
3.4 加载定制化插件

3.4.1 放置定制化插件jar

在服务器或者电脑上创建目录plugins , 将步骤二定义的定制化实现的LDAP认证插件打包jar,然后直接放置到plugins 目录下

3.4.2 配置启动脚本

使用mvn clean package -Dmaven.test.skip=true命令打包cs-server-auth服务,将cs-server-auth服务的jar放置在和plugins目录同级,然后执行如下启动命令:

java  -Dloader.path=./plugins -jar cs-server-auth-1.0.0-SNAPSHOT.jar

通过上述步骤,我们能够动态加载和使用不同的插件实现,从而实现灵活的认证机制。

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值