Tencent/APIJSON

6.3.0 响应驼峰格式问题

Closed this issue · 45 comments

Description

接口地址 /get/sysUserPage

@OverRide
protected String getKey(SQLConfig config, ResultSet rs, ResultSetMetaData rsmd, int tablePosition, JSONObject table, int columnIndex, Map<String, JSONObject> childMap) throws Exception {
String key = super.getKey(config, rs, rsmd, tablePosition, table, columnIndex, childMap);
// 前端传参驼峰命名转为蛇形命名
return JSONResponse.formatUnderline(key, true);
}

放开这段代码 后 错误日志,注释这段代码,可访问,可调用成功

java.lang.IllegalArgumentException: URL 路径 /get/sysUserPage 对应的接口不存在!
at apijson.router.APIJSONRouterController.router(APIJSONRouterController.java:181)
at apijson.router.APIJSONRouterController.router(APIJSONRouterController.java:65)

请求参数

{
"SysUser[]":{
"SysUser":{"realName":"超级管理员"},
"page":0,
"count":10
},
"total@":"/SysUser[]/total",
"format":true
}

应该是用了 apijson-router 但没配置 Document 记录导致
https://github.com/APIJSON/apijson-router?tab=readme-ov-file#usage

image
Document 表是配置了的

调用的地址 /get/sysUserPage 和 配置的 url 不一致

/crud 是接口的前缀

@OverRide
protected String getKey(SQLConfig config, ResultSet rs, ResultSetMetaData rsmd, int tablePosition, JSONObject table, int columnIndex, Map<String, JSONObject> childMap) throws Exception {
String key = super.getKey(config, rs, rsmd, tablePosition, table, columnIndex, childMap);
// 前端传参驼峰命名转为蛇形命名
return JSONResponse.formatUnderline(key, true);
} 注释这段代码 可以正常访问

应该还是此处的问题,注释接口访问正常,去除注释,在访问就不行了

Log.DEBUG = true; 开启后 输出的日志
以上代码开启后 表名也是 解析错了
image

image

断言 formatUnderline 方法,看是不是转换命名格式有问题

当把这里设置为 false 可正常访问 ,就是返回的格式不是驼峰了
image

这里没有调试明白,并没有断点到 sys_user 表

驼峰 请求参数是解析成功的,这是 日志
image

有看到这个错误日志

write javaBean error, fastjson version 1.2.83, class com.quick.online.config.OnlineSQLConfig, write javaBean error, fastjson version 1.2.83, class io.undertow.servlet.core.DeploymentImpl, fieldName : deployment, write javaBean error, fastjson version 1.2.83, class io.undertow.servlet.core.ManagedServlet$DefaultInstanceStrategy$1, fieldName : servlet, write javaBean error, fastjson version 1.2.83, class org.springframework.boot.actuate.endpoint.web.servlet.AdditionalHealthEndpointPathsWebMvcHandlerMapping, fieldName : 0, write javaBean error, fastjson version 1.2.83, class org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext, fieldName : applicationContext, write javaBean error, fastjson version 1.2.83, class org.springframework.web.servlet.resource.ResourceHttpRequestHandler, fieldName : /webjars/**, write javaBean error, fastjson version 1.2.83, class org.springframework.core.io.ClassPathResource, fieldName : 0

这个问题,方便再看下吗

表名转换需要在 DemoSQLConfig 重写 getSQLTable

我重写了
@OverRide
public String getSQLTable() {
String t = super.getSQLTable();
return isInfluxDB() ? t.toLowerCase() : JSONRequest.recoverUnderline(t, false);
}

Log.DEBUG = false; 还是 /get/sysUserPage 对应的接口不存在
Log.DEBUG = true; 可正常访问

返回结果 是大驼峰,我看了 JSONResponse 的源码,没有看到关于小驼峰的格式化方法

最终我是这样实现了 Log.DEBUG = true;
/**
* 统一响应 蛇形命名转小驼峰命名
*/
@OverRide
protected String getKey(SQLConfig config, ResultSet rs, ResultSetMetaData rsmd, int tablePosition, JSONObject table, int columnIndex, Map<String, JSONObject> childMap) throws Exception {
String key = super.getKey(config, rs, rsmd, tablePosition, table, columnIndex, childMap);
return StrUtil.toCamelCase(key);
}

还是 /get/sysUserPage 对应的接口不存在

调用时要用 Document 里配置的 /crud/get/sysUserPage,必须完全一样才行。

Log.DEBUG = true 时为了方便调试,允许万能通用接口 /get,所以走的是 /get/{tag} 接口,而不是 /curd/get/{tag} 接口,可以断点调试 getByTag 和 curdByTag

https://github.com/APIJSON/apijson-framework/blob/master/src/main/java/apijson/framework/APIJSONController.java#L214-L250

还是 /get/sysUserPage 对应的接口不存在

调用时要用 Document 里配置的 /crud/get/sysUserPage

我找到问题了是我写了一个 translateDict 解析字典的函数,返回类型 function 表配置的不一致导致报错,估计是终止加载了

返回结果 是大驼峰,我看了 JSONResponse 的源码,没有看到关于小驼峰的格式化方法

JSONResponse 也需要加一个 toSmallCamelCase, toBigCamelCase 的方法,分别
return StringUtil.firstCase(formatUnderline(key, true), false)

return StringUtil.firstCase(formatUnderline(key, true), true)

get 请求 如何配置允许 调用远程函数, translateDict 主要作用是 解析系统字典值,通过微服务调用字典解析接口,然后put到这个数据对象里

{
    "SysUser[]":{
        "SysUser":{
            "realName":"测试",
            "status()": "translateDict(sys_user_status,status)" 
            
        },
        "page":0,
        "count":10
    },
    "total@":"/SysUser[]/total",
    "format":true
}

继承 APIJSONRouterController 的
请求参数

{
    "SysUser[]":{
        "SysUser":{
            "realName":"测试",
            "status()": "translateDict(sys_user_status,status)" 
            
        },
        "page":0,
        "count":10
    },
    "total@":"/SysUser[]/total",
    "format":true
}

响应参数

{
  "msg":"GET 请求,status() 不合法!非开放请求不允许传远程函数 key():\"fun()\"",
  "status":403,
  "traceId":"65868416f750f4424f0213c05438b169"
}

调试源码 是进入了这里
image

apijson-router 接口不允许前端传远程函数,只允许后端配置,你发的 JSON 请求只能作为 Document.apijson 字段的值,不能作为 Document.request 的值,request 只能是单层键值对,且不包含任何远程函数
https://github.com/APIJSON/apijson-router?tab=readme-ov-file#1add-mapping-rule-in-table-document

后端要如何配置,这块我没有看到文档

以上链接就是文档,认真看看,一步步来,直接用 APIAuto 配置就行,比自己手动配置方便很多,也不容出错

你现在是否要用 apijson-router ?不用的话
DemoController extends APIJSONRouterController 改成 APIJSONController
DemoVerifier extends APIJSONRouterVerifier 改成 APIJSONVerifier
就不存在以上 接口访问不了、远程函数不允许直接前端传参调用 等问题了

要用 apijson-router,因为我需要对每个 TAG 做接口权限控制

url: /crud/get/sysUserPage

request: // 前端调用实际传参

{
     "SysUser[].SysUser.realName":"测试",
     "SysUser[].page":0,
     "SysUser[].count":10,
     "format":true
}

apijson: // 后端内部使用参数

{
    "SysUser[]":{
        "SysUser":{
            "realName":"测试",
            "status()": "translateDict(sys_user_status,status)" 
        },
        "page":0,
        "count":10
    },
    "total@":"/SysUser[]/total",
    "format":true
}

好的,我试试

request: // 前端调用实际传参

{
    "SysUser[].SysUser.realName":"测试",
     "SysUser[].page":0,
     "SysUser[].count":10,
     "format":true
}

apijson: // 后端内部使用参数

{
    "SysUser[]":{
        "SysUser":{
            "realName":"测试",
            "status()": "translateDict(sys_user_status,status)" 
            
        },
        "page":0,
        "count":10
    },
    "total@":"/SysUser[]/total",
    "format":true
}

我按照这个去配置了,实际没有触发调用这个函数

把数据库表配置、调接口界面、代码执行日志 都完整截屏发出来

不要想着一步到位,你现在最好是下载最新的 MultiDataSource Demo,包括代码和 MySQL 数据,除了数据库链接等必要的基础配置,能保证项目跑起来基本功能,其它一点都不要改,先严格按文档一步步把 apijson-router 简单接口走通,然后再按你自己个性化的需求调整路由、驼峰等

你现在改太多东西,遇到问题很难排查,具体是哪里改动引起的,也不好解决

request

INSERT INTO quick-boot.request (id, debug, version, method, tag, structure, detail, date) VALUES (4, 0, 1, 'GET', 'sysUserPage', '{}', '分页查询', '2023-12-18 17:57:33');

document
request
{
"SysUser[].SysUser.realName":"测试",
"SysUser[].page":0,
"SysUser[].count":10,
"format":true
}

apijson

{
"SysUser[]":{
"SysUser":{
"realName":"测试",
"status()": "translateDict(sys_user_status,status)"

    },
    "page":0,
    "count":10
},
"total@":"/SysUser[]/total",
"format":true

}

接口请求 参数

{
"SysUser[].SysUser.realName":"测试",
"SysUser[].page":0,
"SysUser[].count":10,
"format":true
}

错误日志

2023-12-23 15:52:10.009 ERROR [quick-boot-online,658691a9247570cb2c6a4edc6efe2605,2c6a4edc6efe2605] ==> [XNIO-1 task-2] AbstractGlobalExceptionHandler.java.exception - Exception:{}
jakarta.servlet.ServletException: Handler dispatch failed: java.lang.StackOverflowError
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1104)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:547)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at cn.dev33.satoken.filter.SaPathCheckFilterForJakartaServlet.doFilter(SaPathCheckFilterForJakartaServlet.java:55)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)
at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:276)
at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:132)
at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:256)
at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:101)
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:393)
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:859)
at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
at org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.StackOverflowError: null
at java.base/java.lang.StringLatin1.toLowerCase(StringLatin1.java:426)
at java.base/java.lang.String.toLowerCase(String.java:3635)
at java.base/java.lang.String.toLowerCase(String.java:3659)
at apijson.StringUtil.firstCase(StringUtil.java:895)
at apijson.JSONResponse.formatKey(JSONResponse.java:499)
at apijson.JSONResponse.formatObjectKey(JSONResponse.java:452)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)
at apijson.JSONResponse.format(JSONResponse.java:362)

不要想着一步到位,你现在最好是下载最新的 MultiDataSource Demo,包括代码和 MySQL 数据,除了数据库链接等必要的基础配置,能保证项目跑起来基本功能,其它一点都不要改,先严格按文档一步步把 apijson-router 简单接口走通,然后再按你自己个性化的需求调整路由、驼峰等

先按这个来吧,你现在是整合了不少框架和库,加上自己个性化逻辑及配置的改动,不好排查

好的,我调试下

我现在就是接入驼峰有这个问题,没有接入驼峰 已经是跑通了的

我找到问题了,是因为我 translateDict 返回了 current,导致 死循环了

以上问题都已解决,但是我有个疑问,为何使用 apijson-router 请求格式 就需要变更成
{
"SysUser[].page":0,
"SysUser[].count":10,
"format":true
} 这种格式,如果不使用这种格式,就无法映射 apijson
为何不使用 原来的格式 apijson 这种请求格式映射呢?

简单接口就是要简单,如果还能用 APIJSON 的多层嵌套格式,一方面达不到简单的要求,另一方面安全上也不好保证

Java: MultiDataSource 完善驼峰与蛇形命名互转的代码
APIJSON/APIJSON-Demo@5e71493

命名格式转换要排除 APIJSON 系统表,否则可能启动报错