Json序列化对象失败的问题

背景

想要封装一个复杂的对象,然后序列化成json,通过请求工具发送Post请求。因为手动去拼接一个复杂的json很容易出现问题,所以想法是:先封装成对象,然后序列化成json。

过程与现象

网上一般推荐的方法如下:

def toJson(Object obj){
    return JsonOutput.toJson(input)
}

所以我也兴致冲冲的这样写了,但是结果非常不如人意。现象是:我直接运行用groovy代码的main方法跑起来是没有问题的,正常序列化成功;但是在Jenkins pipeline环境下的时候就总是会序列化成一个空对象(即总是返回:{})。

于是在网上搜索各种资料,最后发现很少类似的资料,最后在询问ChatGPT才找到一些关键的线索,ChatGPT回复如下:

jenkins pipeline查询mysql jenkins pipeline try catch_运维


jenkins pipeline查询mysql jenkins pipeline try catch_运维_02


jenkins pipeline查询mysql jenkins pipeline try catch_ci_03


jenkins pipeline查询mysql jenkins pipeline try catch_序列化_04


jenkins pipeline查询mysql jenkins pipeline try catch_序列化_05

jenkins pipeline查询mysql jenkins pipeline try catch_ci_06

总的来说就是get/set方法可能会产生的是动态属性,动态属性在Pipeline中是没法序列化了,所以肯定就是空对象了。

按照这个线索,我不停的测试发现了问题的原因。结论就是:在groovy类定义中如果我们手动重写了get/set方法就会导致对象序列化失败。可能重写了get/set方法在groovy中就是动态属性。

结论与解决办法

在groovy类定义中如果我们不要重写了get/set方法,只要定义属性就好了,groovy定义了属性就自动会有get/set方法。

在测试过程中也发现了几个关于类定义需要注意的点,如下:

  1. 不要写内部静态类,写内部静态类,可能会发生new对象的时候,找不到类信息;
  2. 不要将类写在一个文件里面,不然也会发生找不到类的情况,总的来说就是一个类一个文件;
  3. 定义的类最好是实现Serializable接口,否则也可能在脚本传递中报没法序列化的错误;
  4. 在pipeline里面最好是类似直接写json的方式去定义一个复杂对象,因为脚本化,基本类的复用性不是很强,注意在groovy中是[]包裹不是{}包裹。类定义如下:
def newPostJson(timestamp, sign, title, author, dateStr, projectName, env,
                    buildNumber, jobUrl, detailLog) {
        def testBody = [timestamp: timestamp,
                        sign     : sign,
                        msg_type : "interactive",
                        card     : [
                                type: "template",
                                data: [
                                        template_id      : "ctp_AAumzLoL5YIf",
                                        template_variable: [
                                                title      : title,
                                                author     : author,
                                                time       : dateStr,
                                                projectName: projectName,
                                                env        : env,
                                                buildNumber: buildNumber,
                                                projectUrl : jobUrl,
                                                content    : detailLog
                                        ]
                                ]
                        ]]
        return JsonOutput.toJson(testBody)
    }

空数组定义和Java的有区别,导致报错的问题

背景

请求接口的时候,有些接口需要做签名,并且需要指定sha256算法签名。

过程与现象

很自然就知道百度,google代码;基本代码如下:

private static def sha256(String body, String secret) {
        //使用HmacSHA256算法计算签名
        Mac mac = Mac.getInstance("HmacSHA256")
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"))
        byte[] signData = mac.doFinal(body.getBytes(StandardCharsets.UTF_8))
        return new String(Base64.getEncoder().encode(signData))
    }

但是这次刚刚好签名的内容比较特殊,对一个空字节数组做签名,代码如下:

private static def feishuSign(String secret) {
        String stringToSign = (System.currentTimeMillis() / 1000) + "/n" + secret
        //使用HmacSHA256算法计算签名
        Mac mac = Mac.getInstance("HmacSHA256")
        mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"))
        byte[] signData = mac.doFinal(new byte[]{})
        return new String(Base64.getEncoder().encode(signData))
    }

现象就是:代码编辑器不会报错,但是运行的时候就会报错。 错误如下截图:

jenkins pipeline查询mysql jenkins pipeline try catch_运维_07


然后我将代码转换成java代码,直接运行也是不会报错的。

结论与解决办法

后来尝试了很多次,我后来想到直接定义一个0元素数组是否跟上面定义一致呢?于是在java测试了一下,结果是一致的。最后代码改动如下:

private static def feishuSign(String secret) {
        String stringToSign = (System.currentTimeMillis() / 1000) + "/n" + secret
        //使用HmacSHA256算法计算签名
        Mac mac = Mac.getInstance("HmacSHA256")
        mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"))
        byte[] signData = mac.doFinal(new byte[0])
        return new String(Base64.getEncoder().encode(signData))
    }

就不会报错了,并且结果也是正确的。

CPS的问题

就是有些方法,可能不安全,就会被jenkins的sandbox拦截不让执行,一般错误描述如下:

Scripts not permitted to use staticMethod groovy.json.JsonOutput toJson java.lang.Object. Administrators can decide whether to approve or reject this signature.
[Pipeline] }
[Pipeline] // script
Post stage
[Pipeline] script
[Pipeline] {
[Pipeline] httpRequest
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod groovy.json.JsonOutput toJson java.lang.Object
    at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectStaticMethod(StaticWhitelist.java:243)
    at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onStaticCall(SandboxInterceptor.java:212)
    at org.kohsuke.groovy.sandbox.impl.Checker$2.call(Checker.java:214)
    at org.kohsuke.groovy.sandbox.impl.Checker.checkedStaticCall(Checker.java:218)
    at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:120)
    at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.methodCall(SandboxInvoker.java:17)
    at WorkflowScript.run(WorkflowScript:73)
    at ___cps.transform___(Native Method)
    at com.cloudbees.groovy.cps.impl.ContinuationGroup.methodCall(ContinuationGroup.java:90)
    at com.cloudbees.groovy.cps.impl.FunctionCallBlock$ContinuationImpl.dispatchOrArg(FunctionCallBlock.java:113)
    at com.cloudbees.groovy.cps.impl.FunctionCallBlock$ContinuationImpl.fixArg(FunctionCallBlock.java:83)
    at sun.reflect.GeneratedMethodAccessor366.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.cloudbees.groovy.cps.impl.ContinuationPtr$ContinuationImpl.receive(ContinuationPtr.java:72)
    at com.cloudbees.groovy.cps.impl.LocalVariableBlock$LocalVariable.get(LocalVariableBlock.java:38)
    at com.cloudbees.groovy.cps.LValueBlock$GetAdapter.receive(LValueBlock.java:30)
    at com.cloudbees.groovy.cps.impl.LocalVariableBlock.evalLValue(LocalVariableBlock.java:27)
    at com.cloudbees.groovy.cps.LValueBlock$BlockImpl.eval(LValueBlock.java:55)
    at com.cloudbees.groovy.cps.LValueBlock.eval(LValueBlock.java:16)
    at com.cloudbees.groovy.cps.Next.step(Next.java:83)
    at com.cloudbees.groovy.cps.Continuable$1.call(Continuable.java:177)
    at com.cloudbees.groovy.cps.Continuable$1.call(Continuable.java:166)
    at org.codehaus.groovy.runtime.GroovyCategorySupport$ThreadCategoryInfo.use(GroovyCategorySupport.java:136)
    at org.codehaus.groovy.runtime.GroovyCategorySupport.use(GroovyCategorySupport.java:275)
    at com.cloudbees.groovy.cps.Continuable.run0(Continuable.java:166)
    at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.access$001(SandboxContinuable.java:18)
    at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.run0(SandboxContinuable.java:51)
    at org.jenkinsci.plugins.workflow.cps.CpsThread.runNextChunk(CpsThread.java:187)
    at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.run(CpsThreadGroup.java:420)
    at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.access$400(CpsThreadGroup.java:95)
    at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:330)
    at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:294)
    at org.jenkinsci.plugins.workflow.cps.CpsVmExecutorService$2.call(CpsVmExecutorService.java:67)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at hudson.remoting.SingleLaneExecutorService$1.run(SingleLaneExecutorService.java:139)
    at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:28)
    at jenkins.security.ImpersonatingExecutorService$1.run(ImpersonatingExecutorService.java:68)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:750)
Finished: FAILURE

解决办法

方法一

按照提示点击即可,操作方法如截图:

jenkins pipeline查询mysql jenkins pipeline try catch_序列化_08


到里面页面同意申请即可

jenkins pipeline查询mysql jenkins pipeline try catch_jenkins_09


本人这个已经同意过了,所以没有通过申请的按钮。

方法二

对调用的方法打上注解:@NonCPS,例如:

@NonCPS
static def toJson(Object input) {
    JsonOutput.toJson(input)
}
方法三

手动去系统添加执行权限,操作方法如截图:

jenkins pipeline查询mysql jenkins pipeline try catch_java_10


jenkins pipeline查询mysql jenkins pipeline try catch_运维_11


去里面提交申请执行即可