如何将Nop低代码平台与Solon框架集成

Solon是一个基于Java的国产轻量级微服务框架,详细介绍参见官网solon.noear.org/ 。Solon的启动速度很快,占用内存很小,可以作为SpringBoot的一个替代品。Nop平台是基于可逆计算原理从零开始研发的下一代低代码平台,它的核心是采用面向语言编程范式,为领域特定语言(DSL)的设计和应用提供基础架构支撑。Nop平台是一个上层技术架构平台,它可以运行在多种底层技术框架之上,此前已经适配了Quarkus和Spring框架,在本文中我将以Solon框架为例,介绍Nop平台与第三方框架集成的具体方法。

一. 框架初始化

Solon框架内置了一个轻量级的IoC容器,可以利用它的生命周期管理来触发Nop平台的初始化动作。

java 复制代码
@Component(index = -1)
public class SolonInitializer implements LifecycleBean {
    @Inject
    AppContext appContext;

    @Override
    public void start() throws Throwable {
        io.nop.api.core.ioc.BeanContainer.registerInstance(new SolonBeanContainer(appContext));
        CoreInitialization.initialize();
    }

    @Override
    public void stop() throws Throwable {
        CoreInitialization.destroy();
    }
}

可以通过SolonBeanContainer将Solon的Bean容器适配为Nop平台所需要的IBeanContainer接口。这样在Nop的Bean创建过程中就可以直接使用Solon管理的bean。

Nop提供了一个兼容Spring 1.0配置语法的IoC容器NopIoc,并增加了类似SpringBoot的动态条件匹配机制。为了兼容底层框架的IoC容器,它实际是将底层框架提供的IoC作为自己的parent使用,即在NopIoC中查找不到的bean会在父容器中查找。

关于NopIoc的详细介绍,可以参见如果重写SpringBoot,我们会做哪些不同的选择?

CoreInitialization分阶段初始化

Nop平台使用CoreInitalization.initialize()调用实现平台初始化,它的实现是使用Java的ServiceLoader机制来加载ICoreInitializer接口。 内置的常用初始化器按照如下顺序执行:

  1. ReflectionHelperMethodInitializer: 向反射系统注册扩展函数
  2. XLangCoreInitializer: 注册XLang表达式中使用的全局函数和全局对象
  3. XLangDebuggerInitializer: 启动XLang调试器
  4. ConfigInitializer: 读取application.yaml等配置文件,并从远程配置服务加载
  5. VirtualFileSystemInitializer: 初始化虚拟文件系统
  6. RegisterModelCoreInitializer: 加载register-model.xml模型注册文件,注册DSL模型
  7. DaoDialectInitializer: 扫描读取dialect模型文件
  8. IocCoreInitializer: 初始化NopIoc容器

ICoreInitializer提供了分阶段加载的能力,每个Initializer都具有对应的初始化级别设置,可以明确指定执行到某个初始化级别。例如,在代码生成器中, 我们可以明确指定执行级别为INITIALIZER_PRIORITY_PRECOMPILE,它不会触发NopIoc的初始化。

二. 适配Web服务

Nop平台使用NopGraphQL引擎实现对外暴露的Web服务和RPC服务。与一般的graphql引擎不同,NopGraphQL不仅仅实现了GraphQL调用协议,它还提供了一种通用的服务分解、组合机制,只需要编写一次函数代码,即可将其自动暴露为REST服务、GraphQL服务和gRPC服务等多种服务形式。NopGraphQL采用了最小信息表达的设计,使用它来编写代码实际上比其他服务框架都要更加简单。

java 复制代码
@BizModel("Demo")
public class DemoBizModel {
    @BizQuery
    public DemoResponse testOk(@RequestBean DemoRequest request) {
        DemoResponse ret = new DemoResponse();
        ret.setName(request.getName());
        ret.setResult("ok");
        return ret;
    }
}

上述服务函数可以通过GraphQL协议来调用

graphql 复制代码
query{
   Demo__testOk(name:"sss"){
      name,
      result
   }
}

也可以通过REST请求方式来调用

ini 复制代码
/r/Demo__testOk?name=sss

同时还可以通过gPRC接口来调用

proto3 复制代码
service Demo{
   rpc testOk(DemoRequest)returns (DemoResponse)
}

关于NopGraphQL的详细介绍,可以参见为什么在数学的意义上GraphQL严格的优于REST?

NopGraphQL只需要底层Web框架提供/graphql/r/{operationName}等少数链接的路由映射即可。适配Solon框架时我们只需要实现SolonGraphQLWebService类。

java 复制代码
@Controller
public class SolonGraphQLWebService extends GraphQLWebService {
    @Mapping(path = "/graphql", method = MethodType.POST, produces = "application/json")
    public String graphqlSolon(Context context) throws IOException {
        String body = context.body();
        return FutureHelper.syncGet(runGraphQL(body, this::transformGraphQLResponse));
    }

    protected String transformGraphQLResponse(GraphQLResponseBean response, IGraphQLExecutionContext gqlContext) {
        SolonWebHelper.setResponseHeader(Context.current(), gqlContext.getResponseHeaders());
        return JsonTool.serialize(response, false);
    }

    @Mapping(path = "/r/{@operationName}", method = {MethodType.GET, MethodType.POST}, produces = "application/json")
    public String restSolon(Context context, @Path("@operationName") String operationName) throws IOException {
        String selection = getSelectionParam(context);
        String body = "GET".equalsIgnoreCase(context.method()) ? null : context.body();
        return FutureHelper.syncGet(runRest(null, operationName, () -> {
            return buildRequest(body, selection, true);
        }, this::transformRestResponse));
    }

    protected String transformRestResponse(ApiResponse<?> response, IGraphQLExecutionContext gqlContext) {
        SolonWebHelper.setResponseHeader(Context.current(), response.getHeaders());

        String str = JSON.stringify(response.cloneInstance(false));
        int status = response.getHttpStatus();
        if (status == 0)
            status = 200;

        Context.current().status(status);
        return str;
    }
    ...
}
  • Solon框架将REST Path路径中定义的变量也统一放置到了param集合中,为了避免和其他参数混淆,SolonGraphQLWebService选择使用前缀@用于区分,所以REST路径映射为 /r/{@operationName}
  • 内置的GraphQLWebService提供了基于JAXRS标准对外暴露GraphQL服务的基本框架,我们只需要对它进行一些定制调整即可。
  • NopGraphQL内部实现时只使用通用的getRequestHeader()/setResponseHeader()等函数,不依赖于特定的Web框架对象类,其他的输入输出参数也是纯粹的POJO对象,因此只要通过JSON转换自动进行适配即可。

如果使用Nop平台的安全认证等机制,我们还需要适配HttpServerFilter。因为Nop平台并没有直接使用HttpServletRequest等运行时框架特有的对象类,而是使用自己定义的IHttpServerContext接口,所以我们只需要增加SolonServerContext适配这个接口即可。

java 复制代码
@Component
public class SolonHttpServerFilter implements Filter {
    ...
    @Override
    public void doFilter(Context context, FilterChain chain) throws Throwable {
        List<IHttpServerFilter> serverFilters = getFilters(false);

        if (serverFilters.isEmpty()) {
            chain.doFilter(context);
        } else {
            IHttpServerContext ctx = new SolonServerContext(context);
            HttpServerHelper.runWithFilters(serverFilters, ctx, () -> {
                return FutureHelper.futureCall(() -> {
                    try {
                        chain.doFilter(context);
                        return null;
                    } catch (Error e) {
                        throw e;
                    } catch (Throwable e) {
                        throw NopException.adapt(e);
                    }
                });
            });
        }
    }
}
  • 在Nop平台中,IHttpServerFilter的一个实现类AuthHttpServerFilter负责实现用户访问令牌检查。
  • 与SpringSecurity的实现类似,Nop平台将自身所有的Filter都包装到一起,在一个Solon Filter中执行。

三. 定制静态资源加载器

Solon框架管理js等静态资源时可以开启gzip压缩支持,它的做法是检查是否存在与js文件同目录的js.gz文件,如果存在且浏览器accept支持gzip格式,则返回js.gz文件中的内容。也就是说前台请求app.js,如果存在app.js.gz,则实际返回的是app.js.gz这个压缩后的文件中的内容。

对于Nop平台来说,Solon的这个判断逻辑要求服务端同时提供jsjs.gz两个文件,会显著增大服务端的包大小。Nop平台的前端使用AMIS低代码框架,功能庞大,前端所有js代码压缩后仍然有10M左右的大小(设计器有4M多,前端采用按需加载机制,一般分包为1M左右),而作为内网应用实际上所有的浏览器都具备gzip解码能力,没有必要同时保留两种格式,因此我们一般的做法是只保留js.gz文件。

Solon允许定制静态资源文件和Web请求路径的映射关系,我们可以利用这一点绕过它原有的判断逻辑。

java 复制代码
@Component
public class SolonStaticResourceRegistrar implements LifecycleBean {
    @Override
    public void start() throws Throwable {
        NopResourceRepository repository = new NopResourceRepository();
        //StaticMappings.add("/js/", repository);
        StaticMappings.add("/", repository);
    }
}

我们首先注册一个全局映射关系,要求所有前端路径都经过NopResourceRepository处理。

java 复制代码
public class NopResourceRepository implements StaticRepository {
    @Override
    public URL find(String relativePath) throws Exception {
        if (relativePath.endsWith(".js") || relativePath.endsWith(".css")) {
            URL url = loadResource(relativePath + ".gz");
            if (url != null) {
                return new URL(StringHelper.removeTail(url.toString(), ".gz"));
            }
        }
        return loadResource(relativePath);
    }

    URL loadResource(String path) throws IOException {
        String fullPath = StringHelper.appendPath("META-INF/resources/", path);
        return this.getClass().getClassLoader().getResource(fullPath);
    }
}

在NopResourceRespository中,我们判断如果存在对应的js.gz或者css.gz文件,则直接返回对应的URL,从而跳过对原文件的检查。

四. 封装为Starter模块

Nop平台集成Solon框架的代码被统一封装到nop-solon-starter模块中,在Solon项目中只要引入如下代码即可使用:

xml 复制代码
        <dependency>
            <groupId>io.github.entropy-cloud.extensions</groupId>
            <artifactId>nop-solon-starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

五. 集成效果

Solon框架的启动速度显著快于SpringBoot,打包后的大小也要小得多(大概减少10几M)。如果不使用AMIS前端,nop-solon-service-demo打包后大概21M,其中包含了XLang语言、GraphQL引擎、ORM引擎、报表引擎、工作流引擎、逻辑编排引擎、规则引擎、分布式RPC调用、代码生成器、后台权限管理、动态模型管理等完整的低代码后端服务。

基于可逆计算理论设计的低代码平台NopPlatform已开源:

相关推荐
言之。10 分钟前
Django流式响应
后端·python·django
IT_陈寒16 分钟前
Java 21虚拟线程实战:7个性能翻倍的异步重构案例与避坑指南
前端·人工智能·后端
硅星企鹅17 分钟前
如何使用低代码爬虫工具采集复杂网页数据?
爬虫·python·低代码
不思念一个荒废的名字21 分钟前
【黑马JavaWeb+AI知识梳理】Web后端开发05-SpringAOP
后端
BingoGo29 分钟前
PHP True Async 最近进展以及背后的争议
后端·php
程序员码歌30 分钟前
短思考第264天,每天复盘5分钟,胜过你盲目努力1整年(2)
前端·后端·ai编程
Victor35634 分钟前
Hibernate(3)Hibernate的优点是什么?
后端
Victor35634 分钟前
Hibernate(4)什么是Hibernate的持久化类?
后端
JaguarJack37 分钟前
PHP True Async 最近进展以及背后的争议
后端·php
想不明白的过度思考者3 小时前
Spring Boot 配置文件深度解析
java·spring boot·后端