Final modification of enumeration class

Today, a developer reported a very strange problem to me. It said that the status attribute of an object is an enumeration class. After setting the status of the object and inserting it into the database, the status is gone. It disappears out of thin air and becomes a blank string. . This feels very strange. The conclusion obtained after troubleshooting the entire problem is related to the specifications of the enumeration class.

problem code

Let's first see what the problematic part of the code looks like:

@Override
public String insert(PayRequest payRequest) {
    
    
    // 省略部分无关代码
    PayRequestDO payRequestDO = convertor.toDO(payRequest);
    payMapper.insert(payRequestDO);
    return payRequest.getPayNo();
}

This method is very simple, it is to convert the passed PayRequest object into a PayRequestDO object and then insert it into the database.

PayRequest and PayRequestDO are both ordinary pojo objects, nothing complicated, except that the status of PayRequestDO is replaced by String:

public class PayRequest {
    
    
    private String payId;
    private String payNo;
    private Status status;
    // 省略其它属性和 getter setter
}
public class PayRequestDO {
    
    
    private String payId;
    private String payNo;
    private String status;
    // 省略其它属性和 getter setter
}

As for the PayConvertor#toDO method, it is also very simple, just attribute copy:

public PayRequestDO toDO(PayRequest payRequest) {
    
    
    PayRequestDO payRequestDO = new PayRequestDO();
    payRequestDO.setPayId(payRequest.getPayId());
    payRequestDO.setPayNo(payRequest.getPayNo());
    payRequestDO.setStatus(payRequest.getStatus().getCode());
    
    // 省略其它代码

    return payRequestDO;
}

The developers have repeatedly emphasized that the status in the PayRequest must have a value and be hard-coded. It cannot be empty at all. Then these codes have been checked many times, and there is absolutely no problem with the mapper xml of mybatis. But there is no value when inserted into the database, WHY?

Troubleshooting

First, simply spend some time to eliminate some low-level problems such as writing code but not releasing it, or deploying the wrong version, etc., and make sure that the code running on the server is the code posted above. This is very, very important. Always check The problem is the first thing to do (in fact, most problems can be solved in this step).

Here are two options for quickly determining the online code version:
Option 1: Use the git-commit-id-plugin maven plug-in
Enable spring boot's info actuator:

 配置开放的 Actuator 端点,开放 endpoint 需要注意数据安全,可以配置不同的 management port 或脱敏敏感内容
management.endpoints.web.exposure.include=info

After mvn packge is built and started with java -jar, you can then access localhost:8080/actuator/info to get the current git commit information:

{
    
    
  "git":{
    
    
    "commit":{
    
    
      "time":{
    
    
        "epochSecond":17011234567,
        "nano":0
      },
      "id":"1234567"
    },
    "branch":"master"
  }
}

You can find the specific version of the code through this commit id.

Option 2:
If the git maven plug-in has not been integrated in advance, or the info endpoint has not been opened. You can also download the fat jar package and determine the code version by decompiling it. Of course, there are also some solutions for dumping code online. For example, Arthas, which will appear below, has a jad command, and there is also the HSDB that comes with the JDK. You can also directly dump the classes in the memory to the local disk. If you are interested, you can search by yourself.

------------------------------ Dividing line------------------ ----------

After eliminating low-level problems, let's analyze where the problem lies. Because the symptom of the problem is that the value inserted into the database is lost, we can first look at the digest log analysis of db:

2023-12-04 21:06:04.221|PayCenter|00|8||N|trace8423002774916857900o38o50|payCente

It can be seen from the SQL of the digest log above that the status field in the insert SQL is already passed with '' blank characters. This indicates that the problem does not occur in the ORM framework. The SQL statements written in the XML are excluded here. Wrong question. So the problem must occur in the business method before inserting into the database.

Next, I suspect that the status field in the PayRequest passed in is worthless. I need to look at the attribute values ​​of the PayRequest object in the running thread stack. This scenario is very suitable for using Arthas's watch method. , there is an Arthas plug-in installed in my idea (named Arthas Idea by Wang Xiaoge). We only need to right-click on the insert and select Arthas Command, select the Watch submenu, and then we can get the watch command, which is very convenient.
Insert image description here
Next, we log in to the server, switch to the admin user (Arthas startup requires you to use the same user as java startup), and enter the command just copied:

watch com.xxxxx.paycenter.service.repository.impl.PayRequestRepositoryImpl insert '{
    
    params,returnObj,throwExp}'  -n 1  -x 3

Then we wait for the method to execute until insert, and then we can observe the output of Arthas watch:

method=com.xxxxx.paycenter.service.repository.impl.PayRequestRepositoryImpl$$EnhancerBySpringCGLIB$$4f979dec.insert location=AtExit
ts=2023-11-15 14:54:49; [cost=6.001557ms] result=@ArrayList[
    @Object[][
        @PayRequest[
            serialVersionUID=@Long[1],
            payId=@String[pay2023001],
            payNo=@String[20231114000000001],
            status=@Status[INIT],
            // ...
        ],
    ],
    @String[20231114000000001],
    null,
]

It can be clearly seen that the status attribute still has a value when entering parameters here: Status.INIT

Next, let’s see if the status attribute is still there when the mapper writes data. We also use the Arthas watch command:

watch com.xxxxx.paycenter.infrastructure.dal.mapper.PayMapper insert '{
    
    params,returnObj,throwExp}'  -n 1  -x 3

Then wait for the method to be executed to mapper's insert, and observe what Arthas watch sees:

Affect(class count: 2 , method count: 1) cost in 289 ms, listenerId: 10
method=com.sun.proxy.$Proxy153.insert location=AtExit
ts=2023-11-15 14:51:36; [cost=3.933553ms] result=@ArrayList[
    @Object[][
        @PayRequestDO[
            id=null,
            payId=@String[pay2023002],
            payNo=@String[20231114000000002],
            status=@String[],
            // ...
        ],
    ],
    @Integer[1],
    null,
]

As you can see, it is obvious that status has become empty by the time it is inserted into the database! ! !

is present when entering parameters, but disappears when writing to the database. This means that the only problem is the object conversion method PayConvertor#toDO in the middle.
Use Arthas watch to check the input and output parameters of toDO:

watch com.xxxxx.paycenter.core.convertor.PayConvertor toDO '{
    
    params,returnObj,throwExp}'  -n 1  -x 3 

Output:

method=com.xxxxx.paycenter.core.convertor.PayConvertor.toDO location=AtExit
ts=2023-11-15 15:01:35; [cost=0.432887ms] result=@ArrayList[
    @Object[][
        @PayRequest[
            serialVersionUID=@Long[1],
            payId=@String[pay2023003],
            payNo=@String[20231114000000003],
            status=@Status[INIT],
            // ...
        ],
    ],
    @PayRequestDO[
        id=null,
        payId=@String[pay2023003],
        payNo=@String[20231114000000003],
        status=@String[],
        // ...
    ],
    null,
]

Looking at the output results, the problem does occur inside toDO. After the data conversion, the status attribute is gone.

To be precise, the following line of code loses attributes:

payRequestDO.setStatus(payRequest.getStatus().getCode());

find the reason

So far we have discovered the reason. It is probably that the enumeration class Status behind the status attribute returns null when getCode is used.
The code for Status is as follows:

public enum Status {
    
    

    INIT("INIT", "初始态"),
    
    SUCCESS("SUCCESS", "成功"),

    FAILED("FAILED", "失败"),
    ;

    private String code;

    private String desc;

    Status(String code, String desc) {
    
    
        this.code = code;
        this.desc = desc;
    }

    public String getCode() {
    
    
        return code;
    }

    public void setCode(String code) {
    
    
        this.code = code;
    }

    public String getDesc() {
    
    
        return desc;
    }

    public void setDesc(String desc) {
    
    
        this.desc = desc;
    }
}

Looking at the setter methods of enumeration class attributes, I couldn't help but fall into deep thought: Why does an enumeration class attribute need to provide a setter method?

Generally speaking, the attributes of enumeration classes must be set to final keyword modification, and setter methods cannot be provided. Just imagine if I change the codes of FAILED and SUCCESS through setters as follows, can I still play happily with this code?
Status.FAILED.setCode(“SUCCESS”);

Status.SUCCESS.setCode(“FAILED”);
Obviously the setter call provided here directly destroys the enumeration class, so the best way is to set the enumeration class Properties plus final.

Next, through the '{target}' parameter of watch Status, '{target}' can print the internal status of the object. The result output further verified my conjecture. The code attribute of the partial enumeration of the Status enumeration class has become a blank character. String:


watch com.xxxxx.paycenter.core.enums.Status getCode '{
    
    target}'  -n 1  -x 3

method=com.xxxxx.paycenter.core.enums.Status.getCode location=AtExit
ts=2023-11-15 15:05:36; [cost=0.005638ms] result=@ArrayList[
    @Status[
        INIT=@Status[
            INIT=@Status[INIT],
            SUCCESS=@Status[SUCCESS],
            FAILED=@Status[FAILED],
            code=@String[],
            desc=@String[],
            name=@String[INIT],
            ordinal=@Integer[0],
        ],

root cause

The direct cause has basically been found. Next, we still need to know where and for what need the setCode method of the enumeration class is called. Because I did not find the explicit call in the entire project, I modified the enumeration. setCode, add some code to print the call stack when setCode is called:

public void setCode(String code) {
    
    
    try {
    
    
        throw new RuntimeException();
    } catch (Exception e) {
    
    
      log.error("code before: {}, after: {}", this.code, code, e);
    }
    this.code = code;
}

Some friends reported that throwing exceptions makes the stack look too ugly. This article also provides a solution without throwing exceptions:

public void setCode(String code) {
    
    
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    log.error("code before: {}, after: {}", this.code, code, formatStackTrace(stackTrace));
    this.code = code;
}

public static String formatStackTrace(StackTraceElement[] stackTrace) {
    
    
    StringBuilder stringBuilder = new StringBuilder();
    for (StackTraceElement element : stackTrace) {
    
    
        stringBuilder.append(element.getClassName())
            .append(".")
            .append(element.getMethodName())
            .append("(")
            .append(element.getFileName())
            .append(":")
            .append(element.getLineNumber())
            .append(")")
            .append(System.lineSeparator());
    }
    return stringBuilder.toString();
}

After adding the code, I published it, and the stack was quickly printed:
Insert image description here
This is a problem caused by the third-party library podam (the initiator is the library I introduced), this The function of the library is to pass a class object, parse its attributes, and assign values. Simply put, it generates random attributes of random objects based on the class. The testing tool will use this function. This library parses the enumeration class. It may not be implemented well, resulting in the enumeration's setter method being called through reflection, which ultimately leads to problems.

improvement measures

We investigated the problem of missing attributes inserted from a database, and finally found that the cause of the problem was caused by irregular writing of the enumeration class.
The first thing is to pay attention to the standards when writing code. It is best to install some scanning tools locally, such as sonar. If risks are found, they must be repaired as soon as possible according to the recommendations.
Secondly, podam, a third-party library, still has problems with the implementation of enumeration, and this bug needs to be fixed as soon as possible.

If you scan for problems when submitting code, you can nip problems in the bud.

Insert image description here

Guess you like

Origin blog.csdn.net/weixin_45817985/article/details/134878017