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