Solon 集成 Aifei
说明
2026-06-06 Aifei 框架正式发布了。波总在五一的时候已经放出了 aifei 的正式版和 vip的订阅,作为第一批用户,用上了 aifei-vip-arch 和 aifei-vip,趁着五一学习了一波,顺便让还让AI 生成了Aifei-dev 的 skills,感兴趣的同学可以试一试,https://gitee.com/CrazyAirhead/aifei-dev。当然为了验证这个 skills 我也是做了个示例的,简单的截两张图,看看效果:
因为 Aifei 刚发布,还有很多生态没有起来,而我日常使用的都是 Solon,而且 Solon 生态已经比较完善,又因为非常喜欢 Aifei,于是先把 aifei-enjoy 和 aifei-db 集成到 Solon 生态里面。Aifei 的完全使用就边用边学吧。
集成 aifei-enjoy
由我贡献的 solon-view-aifei-enjoy 插件已经被合并到 Solon 主版本,将会在Solon 的 4.x 版本发布,https://solon.noear.org/article/1456。
因为 Solon 原来就集成了 enjoy,enjoy 又是波总比较新的作品,此次 aifei-enjoy,主要是调整包名,没有太大的变动。所以集成 aifei-enjoy,主要是在 solon-view-enjoy 插件的基础上修复包名。
有一点需要注意的就是enjoy 不支持 Directive 工厂,Solon 的作者自己做一个 enjoy 的扩展,也就是 solon-view-enjoy 不是使用原版的 enjoy 的 jar 包。在 aifei-enjoy 中,提供了原生的支持,所以可以优先使用 Solon 的容器对象。
Engine.setDirectiveFactory(
(cls) -> {
Directive directive = Objects.requireNonNull(Solon.context()).getBean(cls);
if (directive != null) {
return directive;
}
// 兜底
return ClassUtil.newInstance(cls);
});集成 aifei-db
aifei-db-solon-plugin 的插件,我还在开发中,主体功能是基于 activerecord-solon-plugin 改造的,因为 aifei-db 相对于 activerecord 有较大的变动,我要多验证下,暂时还没有签入和提交 PR。
相对于 ActiveRecord,aifei-db 中的 AifeiRow 做了比较多的增强,因此注册AifeiRow被简化。
/**
* @author airhead
*/
public class ModelManager {
@Getter static Set<Class<? extends AifeiRow<?>>> modelSet = new LinkedHashSet<>();
@SuppressWarnings("unchecked")
public static void addModel(Table table, AifeiRow<?> model) {
modelSet.add((Class<? extends AifeiRow<?>>) model.getClass());
}
}另外需要注意的是 aifei-core 不包含 Slf4jLogFactory,因此需要自己扩展一个。
package top.godenyear.ray.framework.aifei.db;
import cn.aifei.log.Log;
import cn.aifei.log.LogFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Aifei LogFactory 的 Slf4j 桥接实现。 解决 aifei-core 不含 Slf4jLogFactory 的问题。
*
* @author airhead
*/
public class Slf4jLogFactory implements LogFactory {
@Override
public Log getLog() {
return getLog(LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME));
}
@Override
public Log getLog(Class<?> clz) {
return getLog(LoggerFactory.getLogger(clz));
}
@Override
public Log getLog(String name) {
return getLog(LoggerFactory.getLogger(name));
}
private Log getLog(Logger slf4jLogger) {
return new Log() {
@Override
public String getName() {
return slf4jLogger.getName();
}
@Override
public boolean isTraceEnabled() {
return slf4jLogger.isTraceEnabled();
}
@Override
public void trace(String msg, Throwable t) {
slf4jLogger.trace(msg, t);
}
@Override
public void trace(String msg) {
slf4jLogger.trace(msg);
}
@Override
public void trace(String format, Object arg) {
slf4jLogger.trace(format, arg);
}
@Override
public void trace(String format, Object arg1, Object arg2) {
slf4jLogger.trace(format, arg1, arg2);
}
@Override
public void trace(String format, Object... args) {
slf4jLogger.trace(format, args);
}
@Override
public void trace(java.util.function.Supplier<String> supplier) {
if (isTraceEnabled()) {
slf4jLogger.trace(supplier.get());
}
}
@Override
public boolean isDebugEnabled() {
return slf4jLogger.isDebugEnabled();
}
@Override
public void debug(String msg, Throwable t) {
slf4jLogger.debug(msg, t);
}
@Override
public void debug(String msg) {
slf4jLogger.debug(msg);
}
@Override
public void debug(String format, Object arg) {
slf4jLogger.debug(format, arg);
}
@Override
public void debug(String format, Object arg1, Object arg2) {
slf4jLogger.debug(format, arg1, arg2);
}
@Override
public void debug(String format, Object... args) {
slf4jLogger.debug(format, args);
}
@Override
public void debug(java.util.function.Supplier<String> supplier) {
if (isDebugEnabled()) slf4jLogger.debug(supplier.get());
}
@Override
public boolean isInfoEnabled() {
return slf4jLogger.isInfoEnabled();
}
@Override
public void info(String msg, Throwable t) {
slf4jLogger.info(msg, t);
}
@Override
public void info(String msg) {
slf4jLogger.info(msg);
}
@Override
public void info(String format, Object arg) {
slf4jLogger.info(format, arg);
}
@Override
public void info(String format, Object arg1, Object arg2) {
slf4jLogger.info(format, arg1, arg2);
}
@Override
public void info(String format, Object... args) {
slf4jLogger.info(format, args);
}
@Override
public void info(java.util.function.Supplier<String> supplier) {
if (isInfoEnabled()) slf4jLogger.info(supplier.get());
}
@Override
public boolean isWarnEnabled() {
return slf4jLogger.isWarnEnabled();
}
@Override
public void warn(String msg, Throwable t) {
slf4jLogger.warn(msg, t);
}
@Override
public void warn(String msg) {
slf4jLogger.warn(msg);
}
@Override
public void warn(String format, Object arg) {
slf4jLogger.warn(format, arg);
}
@Override
public void warn(String format, Object arg1, Object arg2) {
slf4jLogger.warn(format, arg1, arg2);
}
@Override
public void warn(String format, Object... args) {
slf4jLogger.warn(format, args);
}
@Override
public void warn(java.util.function.Supplier<String> supplier) {
if (isWarnEnabled()) {
slf4jLogger.warn(supplier.get());
}
}
@Override
public boolean isErrorEnabled() {
return slf4jLogger.isErrorEnabled();
}
@Override
public void error(String msg, Throwable t) {
slf4jLogger.error(msg, t);
}
@Override
public void error(String msg) {
slf4jLogger.error(msg);
}
@Override
public void error(String format, Object arg) {
slf4jLogger.error(format, arg);
}
@Override
public void error(String format, Object arg1, Object arg2) {
slf4jLogger.error(format, arg1, arg2);
}
@Override
public void error(String format, Object... args) {
slf4jLogger.error(format, args);
}
@Override
public void error(java.util.function.Supplier<String> supplier) {
if (isErrorEnabled()) {
slf4jLogger.error(supplier.get());
}
}
};
}
}处理序列化问题
不管是 ActiveRecord 中 Model,还是Aifei 中的 AifeiRow,AifeiModel,他的底层数据结构是 Map,通过Bean 方法包装后进行 Bean 的操作,而 AifeiModel 更进一步使用了链式的 setter。
在 Solon 文档 https://solon.noear.org/article/18 中说到,Model 转 Json 输入的方法,不过在我的使用过程中体验不是很好。
//应用加载完成事件
@Componentpublic
class AppPluginLoadEndEventListener implements EventListener<AppPluginLoadEndEvent> {
@Overridepublic void onEvent(AppPluginLoadEndEvent e) throws Throwable {
//定制 json 序列化输出(使用新的处理接管 "@json" 指令)
e.app().renders().register("@json", (data, ctx) -> {
String json = JFinalJson.getJson().toJson(data);
ctx.outputAsJson(json);
});
}
}这次在进一步整合 Aifei-db 的过程中,算是找到了一些比较好的集成方式,可以参看Solon 的 issue,重点涉及 EntityConverter 和 PropsConverterExt。
https://gitee.com/opensolon/solon/issues/IJSD9V https://gitee.com/opensolon/solon/issues/IJSCEX
主体的思路是通过完整的替换序列化插件,从而减少扩展链路和调整的地方。
Snack4StringSerializer
mime 为空时也使用JSON序列化。
/**
* 是否匹配
*
* @param ctx 请求上下文
* @param mime 内容类型
*/
@Override
public boolean matched(Context ctx, String mime) {
if (mime == null || mime.isEmpty()) {
return true;
} else {
return mime.contains(label) || mime.startsWith(MimeType.APPLICATION_X_NDJSON_VALUE);
}
}Snack4EntityConverter
changeEntityDo 时使用JSON 进行转换,而不是交给 ClassUtil 处理
/**
* Snack 实体转换器
*
* @author noear
* @since 3.6
*/
public class Snack4EntityConverter extends AbstractStringEntityConverter<Snack4StringSerializer> {
@Override
protected Object changeEntityDo(Context ctx, ParamWrap p, String name, Class<?> type)
throws Exception {
Props props = new Props().addAll(ctx.paramMap());
Options options = serializer.getDeserializeConfig().getOptions();
return ONode.ofBean(props, options).toBean(type);
}
}针对 AifeiRow 提供相应的 encoder
ModelEncoder
/**
* @author airhead
*/
public class ModelEncoder<T extends AifeiRow<?>> implements ObjectPatternEncoder<T> {
@Override
public ONode encode(EncodeContext ctx, T value, ONode node) {
if (value == null) {
return node;
}
return encode(node, value.data());
}
@Override
public boolean canEncode(Object value) {
return value instanceof AifeiRow;
}
protected ONode encode(ONode node, Map<String, Object> map) {
if (CollUtil.isEmpty(map)) {
return node;
}
node.fill(map);
return node;
}
}配置 Snack4StringSerializer
为AifeiRow 指定 ModelEncoder
context.getBeanAsync(
Snack4StringSerializer.class,
serializer -> {
serializer.getDeserializeConfig().addFeatures(Feature.Decode_AllowUseSetter);
serializer.getSerializeConfig().addFeatures(Feature.Write_UseSmlCamelStyle);
serializer.addEncoder(AifeiRow.class, new ModelEncoder<>());
});小结
经过以上的处理,现在应该能在 Solon 里面愉快的玩耍 aifei-enjoy 和 aifei-db了,enjoy 模版,SQL 模板,原来想要的都回来了。虽然 Solon 比 Aifei 的概念会多一些,但整体还是非常克制的,因此即便多了一些 Controller的逻辑,对于AI 的消耗也不会增加太多。
补充
关于 aifei 的一些尝试,可以仓库代码 https://github.com/crazy-airhead/aifei
- 扩展 aifei-json-snack4,替代fastjson2,使用 snack4 进行处理 JSON。
- 扩展 aifei-feathttp, 替代undertow,使用 feathttp(原来的smarthttp)作为底层服务器。
CrazyAirhead