ClassCastException,当我在Spring Boot的POST API中使用对象列表时

ClassCastException when I use a list of objects in a POST API in Spring Boot

提问人:elvis 提问时间:11/8/2023 最后编辑:Tonielvis 更新时间:11/13/2023 访问量:158

问:

我想在Spring Boot项目中为POST请求创建一个简单的REST API。对于此 POST 请求,我需要使用此有效负载:

[
{
 "name": "John",
 "user_name": "johnny",
 "email": "[email protected]",
 "phone": "0000",
 "id": "435445",
 "tag": "zxcvb"
},
{
 "name": "John",
 "user_name": "johnny",
 "email": "[email protected]",
 "phone": "0000",
 "business": "abcd",
 "data": "qwert"
}
]

我无法更改此有效负载模型,有一个包含 2 个对象的列表,这 2 个对象有 4 个公共字段和不同的字段。

我实现了:RestController

@RestController
@Slf4j
public class DataController {
    @PostMapping("/data")
    DataResponse createData(@RequestBody List<CompleteRequest> completeRequests) {
        log.info("completeRequests = {}", completeRequests);
        DataResponse response = new DataResponse();
        response.setName(completeRequests.get(0).getName());
        response.setUserName(completeRequests.get(0).getUserName());
        response.setEmail(completeRequests.get(0).getEmail());
        response.setPhone(completeRequests.get(0).getPhone());
        response.setId(((TagRequest) completeRequests.get(0)).getId());
        response.setTag(((TagRequest) completeRequests.get(0)).getTag());
        response.setBusiness(((DataRequest) completeRequests.get(1)).getBusiness());
        response.setData(((DataRequest) completeRequests.get(1)).getData());
        return response;
    }
}

对于请求正文,我使用一个超类和 2 个子类,因为列表的 2 个对象中有 4 个公共字段。所以我的有效载荷应该是 2 个不同对象的列表。这是一个好方法还是我应该做点别的事情?或者我怎样才能做一个存储 2 个不同对象的列表?

@Getter
@AllArgsConstructor
public class CompleteRequest {
    @JsonProperty("name")
    private String name = null;

    @JsonProperty("user_name")
    private String userName = null;

    @JsonProperty("email")
    private String email = null;

    @JsonProperty("phone")
    private String phone = null;

    @Override
    public String toString() {
        return "CompleteRequest{" +
                "name='" + name + '\'' +
                ", user_name='" + userName + '\'' +
                ", email='" + email + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}


@Getter
public class TagRequest extends CompleteRequest {
    @JsonProperty("id")
    private String id = null;

    @JsonProperty("tag")
    private String tag = null;

    @Builder
    public TagRequest(String name, String userName, String email, String phone, String id, String tag) {
        super(name, userName, email, phone);
        this.id = id;
        this.tag = tag;
    }

    @Override
    public String toString() {
        return "TagRequest{" +
                "id='" + id + '\'' +
                ", tag='" + tag + '\'' +
                '}' + super.toString();
    }
}


@Getter
public class DataRequest extends CompleteRequest {
    @JsonProperty("business")
    private String business = null;

    @JsonProperty("data")
    private String data = null;

    @Builder
    public DataRequest(String name, String userName, String email, String phone, String business, String data) {
        super(name, userName, email, phone);
        this.business = business;
        this.data = data;
    }

    @Override
    public String toString() {
        return "DataRequest{" +
                "business='" + business + '\'' +
                ", data='" + data + '\'' +
                '}' + super.toString();
    }
}

和响应类:

@Getter
@Setter
public class DataResponse {
    @JsonProperty("name")
    private String name = null;

    @JsonProperty("user_name")
    private String userName = null;

    @JsonProperty("email")
    private String email = null;

    @JsonProperty("phone")
    private String phone = null;

    @JsonProperty("id")
    private String id = null;

    @JsonProperty("tag")
    private String tag = null;

    @JsonProperty("business")
    private String business = null;

    @JsonProperty("data")
    private String data = null;
}

但是当我尝试发出 POST 请求时,我得到 500:

2023-11-07 20:56:52.139  INFO 14288 --- [nio-8080-exec-1] com.dgs.restserver.api.DataController    : completeRequests = [CompleteRequest{name='John', user_name='johnny', email='johny@g
mail.com', phone='0000'}, CompleteRequest{name='John', user_name='johnny', email='[email protected]', phone='0000'}]
2023-11-07 20:56:52.157 ERROR 14288 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exc
eption [Request processing failed; nested exception is java.lang.ClassCastException: class com.dgs.restserver.pojo.CompleteRequest cannot be cast to class com.dgs.restserver.pojo.TagRe
quest (com.dgs.restserver.pojo.CompleteRequest and com.dgs.restserver.pojo.TagRequest are in unnamed module of loader java.net.URLClassLoader @5ad6cc21)] with root cause

java.lang.ClassCastException: class com.dgs.restserver.pojo.CompleteRequest cannot be cast to class com.dgs.restserver.pojo.TagRequest (com.dgs.restserver.pojo.CompleteRequest and com.
dgs.restserver.pojo.TagRequest are in unnamed module of loader java.net.URLClassLoader @5ad6cc21)
        at com.dgs.restserver.api.DataController.createData(DataController.java:25) ~[classes/:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) ~[spring-webmvc-5.1.4.RELEASE.jar
:5.1.4.RELEASE]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.1.4.RELEASE.j
ar:5.1.4.RELEASE]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800) ~[spring-webmvc-5.1.4.RELEASE.jar:5.
1.4.RELEASE]
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.4.RELEASE.jar:5.1.4.RELEASE]        
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038) ~[spring-webmvc-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) ~[spring-webmvc-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908) ~[spring-webmvc-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:660) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882) ~[spring-webmvc-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.4.RELEASE.jar:5.1.4.RELEASE]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1417) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.14.jar:9.0.14]
        at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
java spring-boot rest 杰克逊

评论


答:

-2赞 Dmytro Rybalskyi 11/8/2023 #1

发生异常的原因是您尝试将 CompleteRequest 强制转换为 TagRequest,后者是 TagRequest 的父类,无法强制转换。

您可以在 Controller 方法签名中使用 TagRequest。

为每种类型的请求创建单独的“createData”方法可能会更好。

3赞 Toni 11/12/2023 #2

在 REST API 中使用多态类时,Jackson 无法确定应为每个列表元素实例化哪种类型的子类。CompleteRequest

在特定情况下,列表元素将实例化为对象,从而忽略未使用的字段。因此,这些对象不能被强制转换为子类,因此被抛出。CompleteRequestClassCastException

处理此问题的一种简单方法是定义类型提示,如下所示:

@Getter
@AllArgsConstructor
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value=DataRequest.class, name = "DataRequest"),
    @JsonSubTypes.Type(value=TagRequest.class, name = "TagRequest")
})
@NoArgsConstructor
public class CompleteRequest {...}

该属性应添加到请求正文中,Jackson 将使用它来确定要实例化的子类。type

[
    {
        "type": "TagRequest",
        "name": "John",
        "user_name": "johnny",
        "email": "[email protected]",
        "phone": "0000",
        "id": "435445",
        "tag": "zxcvb"
    },
    {
        "type": "DataRequest",
        "name": "John",
        "user_name": "johnny",
        "email": "[email protected]",
        "phone": "0000",
        "business": "abcd",
        "data": "qwert"
    }
]

若要使其正常工作,请将默认构造函数添加到子类中,方法是用 标记它们。@NoArgsConstructor


如果您无法更改传入的有效负载或不能更改,则可以使用 Jackson 版本中引入的注释类型。但是,正如 Xobotun 在他的回答中提到的:DEDUCTION@JsonTypeInfo2.12

我仍然建议使用 ,因为新方法可能会在无法确定使用哪个子类型的复杂情况下引发异常use = JsonTypeInfo.Id.NAME

由于这种情况非常简单,因此您不必担心。

@Getter
@AllArgsConstructor
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
    @JsonSubTypes.Type(value=DataRequest.class),
    @JsonSubTypes.Type(value=TagRequest.class)
})
@NoArgsConstructor
public class CompleteRequest {...}

供参考:使用 Spring Boot 将多态性应用于 REST API

0赞 Valeriy K. 11/12/2023 #3

如果使用“CompleteRequest”作为请求正文,则将丢失“CompleteRequest”类未知的所有数据。因此,您的 JSON 将序列化为请求中的“CompleteRequest”对象,该对象不包含除四个定义的字段之外的任何字段。看:enter image description here

因此,您需要指定一个请求对象,其中包含您可以在 JSON 中发送的所有字段,例如:

@Getter
@AllArgsConstructor
public class CompleteRequest {
    @JsonProperty("name")
    private String name = null;

    @JsonProperty("user_name")
    private String userName = null;

    @JsonProperty("email")
    private String email = null;

    @JsonProperty("phone")
    private String phone = null;

    @JsonProperty("id")
    private String id = null;

    @JsonProperty("tag")
    private String tag = null;

    @JsonProperty("business")
    private String business = null;

    @JsonProperty("data")
    private String data = null;

当您收到请求时,您可以获取必填字段(关于类命名,您的“CompleteRequest”更像是“PartialRequest”。

在本例中,“DataController”将如下所示:

@RestController
@Slf4j
public class DataController {
    @PostMapping("/data")
    DataResponse createData(@RequestBody List<CompleteRequest> completeRequests) {
        log.info("completeRequests = {}", completeRequests);
        DataResponse response = new DataResponse();
        response.setName(completeRequests.get(0).getName());
        response.setUserName(completeRequests.get(0).getUserName());
        response.setEmail(completeRequests.get(0).getEmail());
        response.setPhone(completeRequests.get(0).getPhone());
//        response.setId(((TagRequest) completeRequests.get(0)).getId());
//        response.setTag(((TagRequest) completeRequests.get(0)).getTag());
//        response.setBusiness(((DataRequest) completeRequests.get(1)).getBusiness());
//        response.setData(((DataRequest) completeRequests.get(1)).getData());
        response.setId(completeRequests.get(0).getId());
        response.setTag(completeRequests.get(0).getTag());
        response.setBusiness(completeRequests.get(1).getBusiness());
        response.setData(completeRequests.get(1).getData());
        return response;
    }
}

此外,在请求中处理 null 值(即不设置 null 值)。另一种方法是在请求中只使用 a。它将被强制转换为 LinkedHashMap,您可以将其作为映射进行处理。或者,使用 a 作为请求:List<Object>List<Map<String, String>>

@PostMapping("/data")
    DataResponse createData(@RequestBody List<Map<String, String>> completeRequests) {
        log.info("completeRequests = {}", completeRequests);
        DataResponse response = new DataResponse();
        response.setName(completeRequests.get(0).get("name"));
        response.setUserName(completeRequests.get(0).get("user_name"));
        response.setEmail(completeRequests.get(0).get("email"));
        response.setPhone(completeRequests.get(0).get("phone"));
        response.setId(completeRequests.get(0).get("id"));
        response.setTag(completeRequests.get(0).get("tag"));
        response.setBusiness(completeRequests.get(1).get("business"));
        response.setData(completeRequests.get(1).get("data"));
        return response;
    }