Oinone请求路由源码分析

通过源码分析,从页面发起请求,如果通过graphQL传输到具体action的链路,并且在这之间做了哪些隐式处理
分析源码版本5.1.x

请求流程大致如下:

  1. 拦截所有指定的请求
  2. 组装成graphQL请求信息
  3. 调用graphQL执行
  4. 通过hook拦截先执行
    • RsqlDecodeHook:rsql解密
    • UserHook: 获取用户信息, 通过cookies获取用户ID,再查表获取用户信息,放到本地Local线程里
    • RoleHook: 角色Hook
    • FunctionPermissionHook: 函数权限Hook ,跳过权限拦截的实现放在这一层,对应的配置
    •  pamirs:
           auth:
              fun-filter:
                    - namespace: user.PamirsUserTransient
                      fun: login #登录
                    - namespace: top.PetShop
                      fun: action
                          
    • DataPermissionHook: 数据权限hook
    • PlaceHolderHook:占位符转化替换hook
    • RsqlParseHook: 解释Rsql hook
    • SingletonModelUpdateHookBefore
  5. 执行post具体内容
  6. 通过hook拦截后执行
    • QueryPageHook4TreeAfter: 树形Parent查询优化
    • FieldPermissionHook: 字段权限Hook
    • UserQueryPageHookAfter
    • UserQueryOneHookAfter
  7. 封装执行结果信息返回

时序图

Oinone请求路由源码分析
时序图

核心源码解析

拦截所有指定的请求 /pamirs/模块名
RequestController

    @RequestMapping(
            value = "/pamirs/{moduleName:^[a-zA-Z][a-zA-Z0-9_]+[a-zA-Z0-9]$}",
            method = RequestMethod.POST
    )
    public String pamirsPost(@PathVariable("moduleName") String moduleName,
                             @RequestBody PamirsClientRequestParam gql,
                             HttpServletRequest request,
                             HttpServletResponse response) {
                             }

DefaultRequestExecutor 构建graph请求信息,并调用graph请求


() -> execute(GraphQL::execute, param), param
private <T> T execute(BiFunction<GraphQL, ExecutionInput, T> executor, PamirsRequestParam param) {
    // 获取GraphQL请求信息,包含grapsh schema
    GraphQL graphQL = buildGraphQL(param);
...
    ExecutionInput executionInput = ExecutionInput.newExecutionInput()
                .query(param.getQuery())
                .variables(param.getVariables().getVariables())
                .dataLoaderRegistry(Spider.getDefaultExtension(DataLoaderRegistryApi.class).dataLoader())
                .build();
...
// 调用 GraphQL的方法execute 执行
    T result = executor.apply(graphQL, executionInput);
...
    return result;
    }

QueryAndMutationBinder 绑定graphQL读取写入操作

public static DataFetcher<?> dataFetcher(Function function, ModelConfig modelConfig) {
        if (isAsync()) {
            if (FunctionTypeEnum.QUERY.in(function.getType())) {
                return AsyncDataFetcher.async(dataFetchingEnvironment -> dataFetcherAction(function, modelConfig, dataFetchingEnvironment), ExecutorServiceApi.getExecutorService());
            } else {
                return dataFetchingEnvironment -> dataFetcherAction(function, modelConfig, dataFetchingEnvironment);
            }
        } else {
            return dataFetchingEnvironment -> dataFetcherAction(function, modelConfig, dataFetchingEnvironment);
        }
    }

    private static Object dataFetcherAction(Function function, ModelConfig modelConfig, DataFetchingEnvironment environment) {
        try {
            SessionExtendUtils.tagMainRequest();
            // 使用共享的请求和响应对象
            return Spider.getDefaultExtension(ActionBinderApi.class)
                    .action(modelConfig, function, FunctionTypeEnum.QUERY.in(function.getType()), environment);
        } finally {
            PamirsSession.clearMainSession();
        }
    }

FunctionManager 执行具体方法,会执行前后hook

public Object runProxy(java.util.function.Function<Object[], Object> consumer, Function function, Object... args) {
        SessionMetaBit directive = Objects.requireNonNull(PamirsSession.directive());
        if (directive.isFromClient() || directive.isBuiltAction()) {
            directive.disableFromClient();
            boolean isHook = directive.isHook();
            boolean isDoExtPoint = directive.isDoExtPoint();
            if (isHook) {
                // 执行权限,用户相关的hook
        hookApi.before(function.getNamespace(), function.getFun(), args);
            }
            // 执行业务方法
            Object result = Tx.build(function.getNamespace(), function.getFun()).execute((transactionStatus) -> {
                Object txResult;
                // 植入预制结果集
                String supplier = "ctxHack" + function.getNamespace() + function.getFun();
                Object supplierObj = Optional.ofNullable(PamirsSession.getRequestVariables())
                        .map(PamirsRequestVariables::getVariables)
                        .map(_variables -> _variables.get(supplier))
                        .orElse(null);
                if (null != supplierObj) {
                    try {
                        txResult = ((Supplier<?>) supplierObj).get();
                    } finally {
                        Optional.ofNullable(PamirsSession.getRequestVariables())
                                .map(PamirsRequestVariables::getVariables)
                                .ifPresent(_variables -> _variables.remove(supplier));
                    }
                } else if (isDoExtPoint) {
                    Object[] tArgs = ArrayUtils.toArray(builtinExtPointExecutor.before(function, args));
                    txResult = builtinExtPointExecutor.override(function, consumer, tArgs);
                    builtinExtPointExecutor.callback(function, args);
                    txResult = builtinExtPointExecutor.after(function, txResult);
                } else {
                    txResult = consumer.apply(args);
                }
                return txResult;
            });
            if (isHook) {
                // 执行
                hookApi.after(function.getNamespace(), function.getFun(), result);
            }
            return result;
        } else {
            return Tx.build(function.getNamespace(), function.getFun())
                    .execute((transactionStatus) -> consumer.apply(args));
        }
    }

默认hook 逻辑

  1. RsqlDecodeHook:rsql解密
  2. UserHook:获取用户信息, 通过cookies获取用户ID,再查表获取用户信息,放到本地Local线程里
  3. RoleHook: 角色Hook
  4. FunctionPermissionHook: 函数权限Hook,跳过权限拦截
  5. DataPermissionHook: 数据权限hook
  6. PlaceHolderHook:占位符转化替换hook
  7. RsqlParseHook: 解释Rsql hook
  8. SingletonModelUpdateHookBefore
  9. QueryPageHook4TreeAfter: 树形Parent查询优化
  10. FieldPermissionHook: 字段权限Hook
  11. UserQueryPageHookAfter
  12. UserQueryOneHookAfter

Oinone社区 作者:oinone原创文章,如若转载,请注明出处:https://doc.oinone.top/backend/16247.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(1)
oinone的头像oinone
上一篇 2024年8月20日 pm9:09
下一篇 2024年8月21日 pm10:33

相关推荐

  • 协同开发支持

    协同开发概述 在使用Oinone进行业务开发中,目前开发方式为: 开发各个本地启动项目 与 设计器环境共库共redis的方式进行。 在多个开发人员同时修改一个模型,或者没有及时更新其他同学提交的代码时,存在业务模型创建的数据表字段被删除的情况,协同开发模式正式为解决这个问题而生。 版本支持 4.7.x版本 已经包含分布式支持。 使用步骤 1、业务后端boot工程引入协同开发包 <dependency> <groupId>pro.shushi.pamirs.distribution</groupId> <artifactId>pamirs-distribution-session-cd</artifactId> </dependency> 2、yml文件配置ownSign pamirs: distribution: session: allMetaRefresh: false ownSign: wangxian 配置说明:allMetaRefresh,全量刷新Redis中的元数据,绝大多数情况下都不需要配置;1)第一次启动或者Redis的缓存被清空后,会自动进行全量。2)配置为true表示强制进行全量,一般都不需要配置;3)【推荐】默认增量的方式(即allMetaRefresh: false)写入redis的数据更少,相应的启动速度也更快4)【强制】ownSign是环境隔离的设置,同一个项目组不同的开发人员之间,ownSign配置成不同的(即各自配置成各自的,达到互不干扰) 3、业务系统DB和缓存的约束1)【强制】业务库和设计器Redis共用,包括Redis的前缀,租户和系统隔离键都需要一样(这三个值影响RedisKey的拼接)2)【强制】base库业务系统与设计器共用;3) 【强制】公共库即pamirs (资源-resource、用户-user、权限-auth、文件-file等)共用;4)【强制】「业务库」数据源的别名必须一直,每个开发人员必须配置到自己的本地 或者是远程库库加一个后缀区分; 4、开发同学在各自访问设计器时,URL最后面增加;ownSign=wangxian后回车,ownSign会被保存到浏览器缓存中,后续访问其他的URL访问不需要再次输入;如果需要去掉ownSign的值,则直接把界面上的悬浮窗删掉即可。说明:访问设计URL上增加的ownSign需要与开发各自本地项目yml文件中ownSign的值相同。(每个开发人员各自用各自的ownSign)PS:具体参数配置详见Oinone协同开发使用手册

    2023年12月4日
    96800
  • EIP开放接口使用MD5验签发起请求(v5.x)

    验签工具类 PS:该验签方法仅在pamirs-core的5.0.16版本以上可正常使用 public class EipSignUtils { public static final String SIGN_METHOD_MD5 = "md5"; private static final String SIGN_METHOD_HMAC = "hmac"; private static final String SECRET_KEY_ALGORITHM = "HmacMD5"; private static final String MESSAGE_DIGEST_MD5 = "MD5"; public static String signTopRequest(Map<String, String> params, String secret, String signMethod) throws IOException { // 第一步:检查参数是否已经排序 String[] keys = params.keySet().toArray(new String[0]); Arrays.sort(keys); // 第二步:把所有参数名和参数值串在一起 StringBuilder query = new StringBuilder(); if (SIGN_METHOD_MD5.equals(signMethod)) { query.append(secret); } for (String key : keys) { String value = params.get(key); if (StringUtils.isNoneBlank(key, value)) { query.append(key).append(value); } } // 第三步:使用MD5/HMAC加密 byte[] bytes; if (SIGN_METHOD_HMAC.equals(signMethod)) { bytes = encryptHMAC(query.toString(), secret); } else { query.append(secret); bytes = encryptMD5(query.toString()); } // 第四步:把二进制转化为大写的十六进制(正确签名应该为32大写字符串,此方法需要时使用) return byte2hex(bytes); } private static byte[] encryptHMAC(String data, String secret) throws IOException { byte[] bytes; try { SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), SECRET_KEY_ALGORITHM); Mac mac = Mac.getInstance(secretKey.getAlgorithm()); mac.init(secretKey); bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); } catch (GeneralSecurityException e) { throw new IOException(e.toString(), e); } return bytes; } private static byte[] encryptMD5(String data) throws IOException { return encryptMD5(data.getBytes(StandardCharsets.UTF_8)); } private static byte[] encryptMD5(byte[] data) throws IOException { try { MessageDigest md = MessageDigest.getInstance(MESSAGE_DIGEST_MD5); return md.digest(data); } catch (NoSuchAlgorithmException e)…

    2024年6月29日
    95600
  • Oinone连接外部数据源方案

    场景描述 在实际业务场景中,有是有这样的需求:链接外部数据进行数据的获取;通常的做法:1、【推荐】通过集成平台的数据连接器,链接外部数据源进行数据操作;2、项目代码中链接数据源,即通过程序代码操作外部数据源的数据; 本篇文章只介绍通过程序代码操作外部数据源的方式. 整体方案 Oinone管理外部数据源,即yml中配置外部数据源; 后端通过Mapper的方式进行数据操作(增/删/查/改); 调用Mapper接口的时候,指定到外部数据源; 详细步骤 1、数据源配置(application.yml), 与正常的数据源配置一样 out_ds_name(外部数据源别名): driverClassName: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # local环境配置调整 url: jdbc:mysql://ip(host):端口/数据库Schema?useSSL=false&allowPublicKeyRetrieval=true&useServerPrepStmts=true&cachePrepStmts=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true username: 用户名 password: 命名 initialSize: 5 maxActive: 200 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true asyncInit: true 2、外部数据源其他配置外部数据源限制创建表结构的执行,可以通过配置指定【不创建DB,不创建数据表】 persistence: global: auto-create-database: true auto-create-table: true ds: out_ds_name(外部数据源别名): # 不创建DB auto-create-database: false # 不创建数据表 auto-create-table: false 3、后端写Mapper SQL Mapper跟使用原生mybaits/mybaits-plus写法一样,无特殊限制; Mapper和SQL写到一起,或者分开两个文件都可以 4、Mapper被Service或者Action调用1)启动的Application中@MapperScan需要扫描到对应的包。2)调用是与普通bean一样(即调用方式跟传统的方式样),唯一的区别就是加上DsHintApi,即指定Mapper所使用的数据源。 @Autowired private ScheduleItemMapper scheduleItemMapper; public saveData(Object data) { ScheduleQuery scheduleQuery = new ScheduleQuery(); //scheduleQuery.setActionName(); try (DsHintApi dsHint = DsHintApi.use(“外部数据源名称”)) { List<ScheduleItem> scheduleItems = scheduleItemMapper.selectListForSerial(scheduleQuery); // 具体业务逻辑 } } 其他参考:如何自定义sql语句:https://doc.oinone.top/backend/4759.html

    2024年5月17日
    1.1K00
  • RocketMQ消费者出现类似RemotingTimeoutException: invokeSync call timeout错误处理办法

    RocketMQ消费者在macOS中出现类似RemotingTimeoutException: invokeSync call timeout错误处理办法: 命令行中执行脚本 scutil –set HostName $(scutil –get LocalHostName)  重启应用

    2024年6月12日
    76300
  • 【MSSQL】后端部署使用MSSQL数据库(SQLServer)

    MSSQL数据库配置 驱动配置 Maven配置(2017版本可用) <mssql.version>9.4.0.jre8</mssql.version> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>${mssql.version}</version> </dependency> 离线驱动下载 mssql-jdbc-7.4.1.jre8.jarmssql-jdbc-9.4.0.jre8.jarmssql-jdbc-12.2.0.jre8.jar JDBC连接配置 pamirs: datasource: base: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=base username: xxxxxx password: xxxxxx initialSize: 5 maxActive: 200 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true asyncInit: true 连接url配置 暂无官方资料 url格式 jdbc:sqlserver://${host}:${port};DatabaseName=${database} 在jdbc连接配置时,${database}必须配置,不可缺省。 其他连接参数如需配置,可自行查阅相关资料进行调优。 方言配置 pamirs方言配置 pamirs: dialect: ds: base: type: MSSQL version: 2017 major-version: 2017 pamirs: type: MSSQL version: 2017 major-version: 2017 数据库版本 type version majorVersion 2017 MSSQL 2017 2017 PS:由于方言开发环境为2017版本,其他类似版本原则上不会出现太大差异,如出现其他版本无法正常支持的,可在文档下方留言。 schedule方言配置 pamirs: event: enabled: true schedule: enabled: true dialect: type: MSSQL version: 2017 major-version: 2017 type version majorVersion MSSQL 2017 2017 PS:由于schedule的方言在多个版本中并无明显差异,目前仅提供一种方言配置。 其他配置 逻辑删除的值配置 pamirs: mapper: global: table-info: logic-delete-value: CAST(DATEDIFF(S, CAST('1970-01-01 00:00:00' AS DATETIME), GETUTCDATE()) AS BIGINT) * 1000000 + DATEPART(NS, SYSUTCDATETIME()) / 100 MSSQL数据库用户初始化及授权 — init root user (user name can be modified by oneself) CREATE LOGIN [root] WITH PASSWORD = 'password'; — if using mssql database, this authorization is required. ALTER SERVER ROLE [sysadmin] ADD MEMBER [root];

    2024年10月18日
    69000

Leave a Reply

登录后才能评论