feat: 为插件的安装与卸载提供钩子函数支持

This commit is contained in:
click33 2025-02-24 16:41:35 +08:00
parent 048dadaff7
commit d65881b59a
16 changed files with 469 additions and 81 deletions

View File

@ -0,0 +1,48 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.exception;
/**
* 一个异常代表插件安装过程中出现异常
*
* @author click33
* @since 1.28.0
*/
public class SaTokenPluginException extends SaTokenException {
/**
* 序列化版本号
*/
private static final long serialVersionUID = 6806129545290130131L;
/**
* 一个异常代表插件安装过程中出现异常
* @param message 异常描述
*/
public SaTokenPluginException(String message) {
super(message);
}
/**
* 一个异常代表插件安装过程中出现异常
*
* @param cause 异常对象
*/
public SaTokenPluginException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.fun.hooks;
import cn.dev33.satoken.plugin.SaTokenPlugin;
/**
* SaTokenPlugin 钩子函数
*
* @author click33
* @since 1.41.0
*/
@FunctionalInterface
public interface SaTokenPluginHookFunction<T extends SaTokenPlugin> {
/**
* 执行的方法
* @param plugin 插件实例
*/
void execute(SaTokenPlugin plugin);
}

View File

@ -26,6 +26,13 @@ public interface SaTokenPlugin {
/**
* 安装插件
*/
void setup();
void install();
/**
* 卸载插件
*/
default void destroy(){
}
}

View File

@ -0,0 +1,291 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.plugin;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.exception.SaTokenPluginException;
import cn.dev33.satoken.fun.hooks.SaTokenPluginHookFunction;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
/**
* Sa-Token 插件管理器管理所有插件的加载与卸载
*
* @author click33
* @since 1.41.0
*/
public class SaTokenPluginHolder {
/**
* 默认实例非单例模式可替换
*/
public static SaTokenPluginHolder instance = new SaTokenPluginHolder();
// ------------------- 插件初始化相关 -------------------
/**
* 是否已经加载过插件
*/
public boolean isLoader = false;
/**
* 初始化加载所有插件多次调用只会执行一次
*/
public synchronized void init() {
if(isLoader) {
return;
}
loaderPlugins();
isLoader = true;
}
/**
* 根据 SPI 机制加载所有插件
*/
public synchronized void loaderPlugins() {
SaManager.getLog().info("SPI 插件加载开始 ...");
ServiceLoader<SaTokenPlugin> plugins = ServiceLoader.load(SaTokenPlugin.class);
for (SaTokenPlugin plugin : plugins) {
installPlugin(plugin);
}
SaManager.getLog().info("SPI 插件加载结束 ...");
}
// ------------------- 插件管理 -------------------
/**
* 所有插件的集合
*/
private final List<SaTokenPlugin> pluginList = new ArrayList<>();
/**
* 获取插件集合副本 (拷贝插件集合而非每个插件实例)
* @return /
*/
public synchronized List<SaTokenPlugin> getPluginListCopy() {
return new ArrayList<>(pluginList);
}
/**
* 判断是否已经安装了指定插件
*
* @param pluginClass 插件类型
* @return /
*/
public synchronized<T extends SaTokenPlugin> boolean isInstalledPlugin(Class<T> pluginClass) {
for (SaTokenPlugin plugin : pluginList) {
if (plugin.getClass().equals(pluginClass)) {
return true;
}
}
return false;
}
/**
* 获取指定类型的插件
* @param pluginClass /
* @return /
* @param <T> /
*/
public synchronized<T extends SaTokenPlugin> T getPlugin(Class<T> pluginClass) {
for (SaTokenPlugin plugin : pluginList) {
if (plugin.getClass().equals(pluginClass)) {
return (T) plugin;
}
}
return null;
}
/**
* 消费指定集合的钩子函数
* @param pluginClass /
* @param hooks /
* @param <T> /
*/
protected synchronized <T extends SaTokenPlugin> void _consumeHooks(List<SaTokenPluginHookModel<? extends SaTokenPlugin>> hooks, Class<T> pluginClass) {
for (int i = 0; i < hooks.size(); i++) {
SaTokenPluginHookModel<? extends SaTokenPlugin> model = hooks.get(i);
if(model.listenerClass.equals(pluginClass)) {
model.executeFunction.execute(getPlugin(pluginClass));
hooks.remove(i);
i--;
}
}
}
// ------------------- 插件 Install -------------------
/**
* 安装指定插件
* @param plugin /
*/
public synchronized void installPlugin(SaTokenPlugin plugin) {
// 插件为空拒绝安装
if (plugin == null) {
throw new SaTokenPluginException("插件不可为空");
}
// 插件已经被安装过了拒绝再次安装
if (isInstalledPlugin(plugin.getClass())) {
throw new SaTokenPluginException("插件 [ " + plugin.getClass().getCanonicalName() + " ] 已安装,不可重复安装");
}
// 执行该插件的 install 前置钩子
_consumeHooks(beforeInstallHooks, plugin.getClass());
// 插件安装
plugin.install();
// 执行该插件的 install 后置钩子
_consumeHooks(afterInstallHooks, plugin.getClass());
// 添加到插件集合
pluginList.add(plugin);
}
/**
* 安装指定插件根据插件类型
* @param pluginClass /
*/
public synchronized<T extends SaTokenPlugin> void installPlugin(Class<T> pluginClass) {
try {
T plugin = pluginClass.getDeclaredConstructor().newInstance();
installPlugin(plugin);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new SaTokenPluginException(e);
}
}
/**
* 插件 [ Install 前置钩子 ] 集合
*/
private final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> beforeInstallHooks = new ArrayList<>();
/**
* 插件 [ Install 后置钩子 ] 集合
*/
private final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> afterInstallHooks = new ArrayList<>();
/**
* 注册指定插件的 [ Install 前置钩子 ]同插件支持多次注册如果插件已经安装完毕则抛出异常
* @param listenerClass /
* @param executeFunction /
* @param <T> /
*/
public synchronized<T extends SaTokenPlugin> void onBeforeInstall(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {
// 如果指定的插件已经安装完毕则不再允许注册前置钩子函数
if(isInstalledPlugin(listenerClass)) {
throw new SaTokenPluginException("插件 [ " + listenerClass.getCanonicalName() + " ] 已安装完毕,不允许再注册前置钩子函数");
}
// 堆积到钩子函数集合
beforeInstallHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));
}
/**
* 注册指定插件的 [ Install 后置钩子 ]同插件支持多次注册如果插件已经安装完毕则立即执行该钩子函数
* @param listenerClass /
* @param executeFunction /
* @param <T> /
*/
public synchronized<T extends SaTokenPlugin> void onAfterInstall(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {
// 如果指定的插件已经安装完毕则立即执行该钩子函数
if(isInstalledPlugin(listenerClass)) {
executeFunction.execute(getPlugin(listenerClass));
return;
}
// 堆积到钩子函数集合
afterInstallHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));
}
// ------------------- 插件 Destroy -------------------
/**
* 卸载指定插件
* @param plugin /
*/
public synchronized void destroyPlugin(SaTokenPlugin plugin) {
// 插件为空拒绝卸载
if (plugin == null) {
throw new SaTokenPluginException("插件不可为空");
}
// 插件未被安装拒绝卸载
if (!isInstalledPlugin(plugin.getClass())) {
throw new SaTokenPluginException("插件 [ " + plugin.getClass().getCanonicalName() + " ] 未安装,无法卸载");
}
// 执行该插件的 destroy 前置钩子
_consumeHooks(beforeDestroyHooks, plugin.getClass());
// 插件卸载
plugin.destroy();
// 执行该插件的 destroy 后置钩子
_consumeHooks(afterDestroyHooks, plugin.getClass());
}
/**
* 卸载指定插件根据插件类型
* @param pluginClass /
*/
public synchronized<T extends SaTokenPlugin> void destroyPlugin(Class<T> pluginClass) {
destroyPlugin(getPlugin(pluginClass));
}
/**
* 插件 [ Destroy 前置钩子 ] 集合
*/
private final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> beforeDestroyHooks = new ArrayList<>();
/**
* 插件 [ Destroy 后置钩子 ] 集合
*/
private final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> afterDestroyHooks = new ArrayList<>();
/**
* 注册指定插件的 [ Destroy 前置钩子 ]同插件支持多次注册
* @param listenerClass /
* @param executeFunction /
* @param <T> /
*/
public synchronized<T extends SaTokenPlugin> void onBeforeDestroy(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {
beforeDestroyHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));
}
/**
* 注册指定插件的 [ Destroy 后置钩子 ]同插件支持多次注册
* @param listenerClass /
* @param executeFunction /
* @param <T> /
*/
public synchronized<T extends SaTokenPlugin> void onAfterDestroy(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {
afterDestroyHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.plugin;
import cn.dev33.satoken.fun.hooks.SaTokenPluginHookFunction;
/**
* Sa-Token 插件 Hook Model
*
* @author click33
* @since 1.41.0
*/
public class SaTokenPluginHookModel<T extends SaTokenPlugin> {
/**
* 监听插件类型
*/
public Class<T> listenerClass;
/**
* 执行的方法
*/
public SaTokenPluginHookFunction<T> executeFunction;
/**
* 构造函数
* @param listenerClass /
* @param executeFunction /
*/
public SaTokenPluginHookModel(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {
this.listenerClass = listenerClass;
this.executeFunction = executeFunction;
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.plugin;
import cn.dev33.satoken.SaManager;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
/**
* Sa-Token 插件加载器管理所有插件的加载
*
* @author click33
* @since 1.41.0
*/
public class SaTokenPluginLoader {
/**
* 是否已经加载过插件
*/
public static boolean isLoader = false;
/**
* 所有插件的集合
*/
public static List<SaTokenPlugin> pluginList;
/**
* 初始化加载所有插件多次调用只会执行一次
*/
public static void init() {
if(isLoader) {
return;
}
loaderPlugins();
isLoader = true;
}
/**
* 根据 SPI 机制加载所有插件
*/
public static void loaderPlugins() {
SaManager.getLog().info("SPI 插件加载开始 ...");
List<SaTokenPlugin> list = new ArrayList<>();
ServiceLoader<SaTokenPlugin> plugins = ServiceLoader.load(SaTokenPlugin.class);
for (SaTokenPlugin plugin : plugins) {
plugin.setup();
list.add(plugin);
}
pluginList = list;
SaManager.getLog().info("SPI 插件加载结束 ...");
}
}

View File

@ -3,6 +3,8 @@ package com.pj.satoken;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.plugin.SaTokenPluginForJackson;
import cn.dev33.satoken.plugin.SaTokenPluginHolder;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.util.SaResult;
@ -84,4 +86,24 @@ public class SaTokenConfigure implements WebMvcConfigurer {
;
}
/**
* 注册 [Sa-Token 全局过滤器]
*/
@Bean
public SaTokenPluginHolder getSaTokenPluginHolder() {
System.out.println("自定义逻辑");
SaTokenPluginHolder.instance.onBeforeInstall(SaTokenPluginForJackson.class, plugin -> {
System.out.println("自定义逻辑前置");
});
SaTokenPluginHolder.instance.onAfterInstall(SaTokenPluginForJackson.class, plugin -> {
System.out.println("自定义逻辑后");
});
SaTokenPluginHolder.instance.onAfterInstall(SaTokenPluginForJackson.class, plugin -> {
System.out.println("自定义逻辑后置2");
});
return SaTokenPluginHolder.instance;
}
}

View File

@ -27,7 +27,7 @@ import cn.dev33.satoken.context.dubbo.SaTokenSecondContextForDubbo;
public class SaTokenPluginForDubbo implements SaTokenPlugin {
@Override
public void setup() {
public void install() {
SaManager.setSaTokenSecondContext(new SaTokenSecondContextForDubbo());
}

View File

@ -27,7 +27,7 @@ import cn.dev33.satoken.context.dubbo3.SaTokenSecondContextForDubbo3;
public class SaTokenPluginForDubbo3 implements SaTokenPlugin {
@Override
public void setup() {
public void install() {
SaManager.setSaTokenSecondContext(new SaTokenSecondContextForDubbo3());
}

View File

@ -29,7 +29,7 @@ import cn.dev33.satoken.strategy.SaStrategy;
public class SaTokenPluginForFastjson implements SaTokenPlugin {
@Override
public void setup() {
public void install() {
// 设置JSON转换器Fastjson
SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson());

View File

@ -29,7 +29,7 @@ import cn.dev33.satoken.strategy.SaStrategy;
public class SaTokenPluginForFastjson2 implements SaTokenPlugin {
@Override
public void setup() {
public void install() {
// 设置 JSON 转换器Fastjson2
SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson2());

View File

@ -27,7 +27,7 @@ import cn.dev33.satoken.dao.SaTokenDaoForHutoolTimedCache;
public class SaTokenPluginForHutoolCache implements SaTokenPlugin {
@Override
public void setup() {
public void install() {
SaManager.setSaTokenDao(new SaTokenDaoForHutoolTimedCache());

View File

@ -27,7 +27,7 @@ import cn.dev33.satoken.json.SaJsonTemplateForJackson;
public class SaTokenPluginForJackson implements SaTokenPlugin {
@Override
public void setup() {
public void install() {
SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson());
}

View File

@ -27,7 +27,7 @@ import cn.dev33.satoken.temp.jwt.SaTempForJwt;
public class SaTokenPluginForTempForJwt implements SaTokenPlugin {
@Override
public void setup() {
public void install() {
SaManager.setSaTemp(new SaTempForJwt());
}

View File

@ -16,6 +16,7 @@
package cn.dev33.satoken.jfinal;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.dao.auto.SaTokenDaoBySessionFollowObject;
import cn.dev33.satoken.util.SaFoxUtil;
import com.jfinal.plugin.redis.Cache;
import com.jfinal.plugin.redis.Redis;
@ -26,7 +27,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class SaTokenDaoRedis implements SaTokenDao {
public class SaTokenDaoRedis implements SaTokenDaoBySessionFollowObject {
protected Cache redis;
protected ISerializer serializer;

View File

@ -29,7 +29,7 @@ import cn.dev33.satoken.json.SaJsonTemplate;
import cn.dev33.satoken.listener.SaTokenEventCenter;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.log.SaLog;
import cn.dev33.satoken.plugin.SaTokenPluginLoader;
import cn.dev33.satoken.plugin.SaTokenPluginHolder;
import cn.dev33.satoken.same.SaSameTemplate;
import cn.dev33.satoken.serializer.SaSerializerTemplate;
import cn.dev33.satoken.sign.SaSignTemplate;
@ -61,8 +61,9 @@ public class SaBeanInject {
* @param saTokenConfig 配置对象
*/
public SaBeanInject(
@Autowired(required = false) SaLog log,
@Autowired(required = false) SaTokenConfig saTokenConfig
@Autowired(required = false) SaLog log,
@Autowired(required = false) SaTokenConfig saTokenConfig,
@Autowired(required = false) SaTokenPluginHolder pluginHolder
){
if(log != null) {
SaManager.setLog(log);
@ -71,7 +72,11 @@ public class SaBeanInject {
SaManager.setConfig(saTokenConfig);
}
// 初始化 Sa-Token SPI 插件
SaTokenPluginLoader.init();
if (pluginHolder == null) {
pluginHolder = SaTokenPluginHolder.instance;
}
pluginHolder.init();
SaTokenPluginHolder.instance = pluginHolder;
}
/**