Solon 集成 Aifei

说明 ​

2026-06-06 Aifei 框架正式发布了。波总在五一的时候已经放出了 aifei 的正式版和 vip的订阅,作为第一批用户,用上了 aifei-vip-arch 和 aifei-vip,趁着五一学习了一波,顺便让还让AI 生成了Aifei-dev 的 skills,感兴趣的同学可以试一试,https://gitee.com/CrazyAirhead/aifei-dev。当然为了验证这个 skills 我也是做了个示例的,简单的截两张图,看看效果:

img

img

因为 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 的容器对象。

Java
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。

img

相对于 ActiveRecord,aifei-db 中的 AifeiRow 做了比较多的增强,因此注册AifeiRow被简化。

TypeScript
/**
 * @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,因此需要自己扩展一个。

Java
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 输入的方法,不过在我的使用过程中体验不是很好。

Java
//应用加载完成事件
@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序列化。

Java
/**
 * 是否匹配
 *
 * @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 处理

Java
/**
 * 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

TypeScript
/**
 * @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

Java
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

  1. 扩展 aifei-json-snack4,替代fastjson2,使用 snack4 进行处理 JSON。
  2. 扩展 aifei-feathttp, 替代undertow,使用 feathttp(原来的smarthttp)作为底层服务器。