feat: 完善 Skill 管理与发布治理

- 实现标准资源存储、能力绑定及双格式导入导出

- 接入分类、可见范围、审批发布与资源权限校验

- 补充并发、租户隔离、安全边界和迁移契约测试
This commit is contained in:
2026-07-27 18:54:20 +08:00
parent aedefe6b5e
commit 2a9e882ac6
165 changed files with 23737 additions and 1088 deletions

View File

@@ -0,0 +1,82 @@
package tech.easyflow.common.cache;
import com.alicp.jetcache.anno.SerialPolicy;
import com.alicp.jetcache.support.CacheEncodeException;
import com.alicp.jetcache.support.JavaValueDecoder;
import org.springframework.core.ConfigurableObjectInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Objects;
/**
* 使用应用类加载器反序列化 JetCache Java 缓存值。
*
* <p>异步线程的上下文类加载器可能无法访问 Spring Boot 可执行包中的嵌套依赖,
* 因此解码时固定使用本类的定义类加载器。</p>
*/
public class ApplicationClassLoaderJavaValueDecoder extends JavaValueDecoder {
private final ClassLoader applicationClassLoader;
/**
* 创建使用 EasyFlow 应用类加载器的 Java 缓存解码器。
*/
public ApplicationClassLoaderJavaValueDecoder() {
this(ApplicationClassLoaderJavaValueDecoder.class.getClassLoader());
}
/**
* 创建使用指定类加载器的 Java 缓存解码器。
*
* @param applicationClassLoader 反序列化缓存对象时使用的类加载器
* @throws NullPointerException 类加载器为空时抛出
*/
ApplicationClassLoaderJavaValueDecoder(ClassLoader applicationClassLoader) {
super(true);
this.applicationClassLoader = Objects.requireNonNull(
applicationClassLoader,
"applicationClassLoader must not be null"
);
}
/**
* 解码带 JetCache Java 编码标识的缓存值。
*
* @param buffer Redis 中读取的缓存字节
* @return 反序列化后的缓存对象
* @throws CacheEncodeException 缓存内容为空、编码类型不匹配或反序列化失败时抛出
*/
@Override
public Object apply(byte[] buffer) {
try {
if (buffer == null || buffer.length < Integer.BYTES) {
throw new CacheEncodeException("decode error: invalid java cache payload");
}
int identityNumber = parseHeader(buffer);
if (identityNumber != SerialPolicy.IDENTITY_NUMBER_JAVA) {
throw new CacheEncodeException(
"decode error: unsupported cache identity number " + identityNumber
);
}
return doApply(buffer);
} catch (CacheEncodeException e) {
throw e;
} catch (Throwable e) {
throw new CacheEncodeException("decode error", e);
}
}
/**
* 创建绑定应用类加载器的对象输入流。
*
* @param input 缓存对象字节输入流
* @return 可从应用依赖中解析类的对象输入流
* @throws IOException 对象输入流初始化失败时抛出
*/
@Override
protected ObjectInputStream buildObjectInputStream(ByteArrayInputStream input) throws IOException {
return new ConfigurableObjectInputStream(input, applicationClassLoader);
}
}

View File

@@ -10,6 +10,11 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.function.Function;
/**
* EasyFlow 缓存基础配置。
*/
@Configuration
public class CacheConfig {
@@ -20,6 +25,9 @@ public class CacheConfig {
private Cache<String, Object> defaultCache;
/**
* 根据平台配置初始化默认缓存。
*/
@PostConstruct
public void init() {
CacheType type = CacheType.LOCAL;
@@ -35,8 +43,23 @@ public class CacheConfig {
defaultCache = cacheManager.getOrCreateCache(quickConfig);
}
/**
* 获取平台默认缓存。
*
* @return 默认缓存实例
*/
@Bean("defaultCache")
public Cache<String, Object> getDefaultCache() {
return defaultCache;
}
/**
* 创建固定使用应用类加载器的 JetCache Java 解码器。
*
* @return JetCache 缓存值解码函数
*/
@Bean("easyFlowJetCacheValueDecoder")
public static Function<byte[], Object> easyFlowJetCacheValueDecoder() {
return new ApplicationClassLoaderJavaValueDecoder();
}
}

View File

@@ -0,0 +1,60 @@
package tech.easyflow.common.cache;
import com.alicp.jetcache.CacheValueHolder;
import com.alicp.jetcache.support.JavaValueEncoder;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* {@link ApplicationClassLoaderJavaValueDecoder} 回归测试。
*/
public class ApplicationClassLoaderJavaValueDecoderTest {
/**
* 验证异步线程上下文类加载器不可见应用依赖时仍可解码缓存值。
*
* @throws Exception 异步任务执行失败时抛出
*/
@Test
public void applyShouldUseApplicationClassLoaderInAsyncThread() throws Exception {
ApplicationClassLoaderJavaValueDecoder decoder = new ApplicationClassLoaderJavaValueDecoder();
CacheValueHolder<String> holder = new CacheValueHolder<>("workflow-state", TimeUnit.MINUTES.toMillis(1));
byte[] encoded = new JavaValueEncoder(true).apply(holder);
ClassLoader isolatedClassLoader = new ClassLoader(null) {
};
assertClassIsInvisible(isolatedClassLoader, CacheValueHolder.class.getName());
ExecutorService executor = Executors.newSingleThreadExecutor(task -> {
Thread thread = new Thread(task, "jetcache-decoder-test");
thread.setContextClassLoader(isolatedClassLoader);
return thread;
});
try {
Object decoded = executor.submit(() -> decoder.apply(encoded)).get(5, TimeUnit.SECONDS);
Assert.assertTrue(decoded instanceof CacheValueHolder<?>);
Assert.assertEquals("workflow-state", ((CacheValueHolder<?>) decoded).getValue());
} finally {
executor.shutdownNow();
}
}
/**
* 验证指定类加载器无法加载目标类。
*
* @param classLoader 待验证类加载器
* @param className 目标类名
*/
private void assertClassIsInvisible(ClassLoader classLoader, String className) {
try {
classLoader.loadClass(className);
Assert.fail("isolated class loader should not load " + className);
} catch (ClassNotFoundException expected) {
// 隔离类加载器符合测试前提。
}
}
}