在 SpringBoot 中尝试在响应正文中返回 List<UUID> 时出现 HttpMessageNotWritableException

问题描述 投票:0回答:1

如果我尝试返回从实体类中的字段获取的列表,我会收到以下错误。如果我自己创建列表,那么我不会收到此错误。我正在使用 SpringBoot 3.3.2 和 Java 17。

错误:

{
  "message": "Exception occurred: HttpMessageNotWritableException: Could not write JSON: class java.lang.String cannot be cast to class java.util.UUID (java.lang.String and java.util.UUID are in module java.base of loader 'bootstrap')",
}

带有堆栈跟踪的完整错误日志:

{
  "@timestamp": "2024-08-10T10:43:56.580637+05:30",
  "@version": "1",
  "message": "Exception stack trace: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: class java.lang.String cannot be cast to class java.util.UUID (java.lang.String and java.util.UUID are in module java.base of loader 'bootstrap')\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:492)\n\tat org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:114)\n\tat org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:297)\n\tat org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:245)\n\tat org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:136)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat com.github.foodie.config.FilterConfig.doFilter(FilterConfig.java:26)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:389)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)\n\tat java.base/java.lang.Thread.run(Thread.java:840)\nCaused by: com.fasterxml.jackson.databind.JsonMappingException: class java.lang.String cannot be cast to class java.util.UUID (java.lang.String and java.util.UUID are in module java.base of loader 'bootstrap') (through reference chain: com.github.foodie.controllers.response.GenericResponse[\"data\"]->com.github.foodie.dtos.TestDto1[\"testDto2\"]->com.github.foodie.dtos.TestDto2[\"uuidList2\"]->java.util.ArrayList[0])\n\tat com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:402)\n\tat com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:373)\n\tat com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:345)\n\tat com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContentsUsing(IndexedListSerializer.java:148)\n\tat com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:88)\n\tat com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)\n\tat com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)\n\tat com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732)\n\tat com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770)\n\tat com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:183)\n\tat com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732)\n\tat com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770)\n\tat com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:183)\n\tat com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732)\n\tat com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770)\n\tat com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:183)\n\tat com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:502)\n\tat com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:341)\n\tat com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1574)\n\tat com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1061)\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:483)\n\t... 51 more\nCaused by: java.lang.ClassCastException: class java.lang.String cannot be cast to class java.util.UUID (java.lang.String and java.util.UUID are in module java.base of loader 'bootstrap')\n\tat com.fasterxml.jackson.databind.ser.std.UUIDSerializer.serialize(UUIDSerializer.java:24)\n\tat com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContentsUsing(IndexedListSerializer.java:142)\n\t... 68 more\n",
  "logger_name": "com.github.foodie.advices.ExceptionAdvice",
  "thread_name": "http-nio-8082-exec-3",
  "level": "INFO",
  "level_value": 20000,
  "correlationId": "1667a21f-358f-4066-a3bb-bc6c19cacb59"
}

控制器类 出于测试目的,我直接从此处调用 OrderRequestEntity,并确保数据库中存在给定 UUID 的实体。您可以在本文中的 TestDto1、TestDto2 和 GenericResponse 之后找到 OrderRequestEntity 类。

    @GetMapping("/res1")
    private ResponseEntity<GenericResponse<TestDto1>> responseTest1(@RequestParam Integer num) {
        TestDto2 testDto2 = new TestDto2();
        List<UUID> uuidList = new ArrayList<>();
        switch (num) {
            case 1: {
                uuidList.add(UUID.randomUUID());
                uuidList.add(UUID.randomUUID());
                break;
            }
            case 2: {
                OrderRequestEntity entity = orderRequestRepository.findById(UUID.fromString("158790c2-ce63-4d00-b053-33f5a72782e7")).orElse(null);
                uuidList = entity.getItems();
            }
        }

        testDto2.setId(UUID.randomUUID());
       testDto2.setUuidList2(uuidList);

        TestDto1 testDto1 = new TestDto1();
        testDto1.setId(UUID.randomUUID());
        testDto1.setTestDto2(testDto2);
        return ResponseEntity.ok(new GenericResponse<>(true, "response got", testDto1));
    }

测试Dto1


@Data
@AllArgsConstructor
@NoArgsConstructor
public class TestDto1 {
    private UUID id;
    private TestDto2 testDto2;;
}

测试Dto2

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TestDto2 {
    private UUID id;
    private List<UUID>  uuidList2;
    private Point point1;
    private Point point2;
}

通用响应

@Data
@NoArgsConstructor
@AllArgsConstructor
public class GenericResponse<T> {
    private boolean success;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String message;
    private String correlationId = MDC.get("correlationId");
    private T data;

    public GenericResponse(boolean success, String message, T data) {
        this.success = success;
        this.message = message;
        this.data = data;
    }

    public GenericResponse(T data) {
        this.success = true;
        this.data = data;
    }
}

OrderRequestEntity(这里我感兴趣的字段是“items”,我为此编写了转换器,当我们将其存储在数据库中时将其转换为 JSON,当我们尝试从中获取项目时将其转换回列表实体)。

@Data
@Entity
@Table(name = "order_request")
public class OrderRequestEntity {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private UUID id;

    @Column(name = "restaurant_location", columnDefinition = "Geometry(Point, 4326)")
    private Point restaurantLocation;

    @Column(name = "delivery_location", columnDefinition = "Geometry(Point, 4326)")
    private Point deliveryLocation;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private CustomerEntity customerEntity;

    @Column(name = "customer_id", updatable = false, insertable = false)
    private UUID customerId;

    @Column(name = "restaurant_id")
    private UUID restaurantId;

    @Convert(converter = UUIDListConvertor.class)
    @Column(name = "items")
    private List<UUID> items;

    @Enumerated(EnumType.STRING)
    @Column(name = "payment_type")
    private PaymentType paymentType;

    @Enumerated(EnumType.STRING)
    @Column(name = "order_request_status")
    private OrderRequestStatusType orderRequestStatusType;

    @Column(name = "created_on")
    private Instant createdOn;

    @Column(name = "updated_on")
    private Instant updatedOn;

    @PrePersist
    protected void onCreate() {
        this.createdOn = this.updatedOn = Instant.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedOn = Instant.now();
    }
}

UUID列表转换器

@Converter
public class UUIDListConvertor extends JsonConverter<List<UUID>> {

}

Json转换器

@Converter
public class JsonConverter<T> implements AttributeConverter<T, String> {
    private final TypeReference<T> typeReference = new TypeReference<T>() {};
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(T object) {
        try {
            return objectMapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Failed to serialize JSON data", e);
        }
    }

    @Override
    public T convertToEntityAttribute(String json) {
        try {
            return objectMapper.readValue(json, typeReference);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Failed to deserialize JSON data", e);
        }
    }
}

pom.xml


<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-database-postgresql</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-spatial</artifactId>
            <version>6.5.2.Final</version>
        </dependency>
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>net.logstash.logback</groupId>
            <artifactId>logstash-logback-encoder</artifactId>
            <version>8.0</version>
        </dependency>
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.15.0</version>
        </dependency>

预期结果是 List 在 TestDto2 中正确设置,我们也得到它的响应。但是当我从 OrderRequestEntity 设置它时,我收到上面共享的错误。当我们手动将 uuid 添加到列表中时,不会出现此错误。我怀疑这是由于与 OrderRequestEntity 中的 items 字段关联的转换器所致。

为了检查这是否是问题,我使用了调试器,我观察到在 TestDto2 的列表 uuidList2 中成功设置了值,但当程序到达末尾给出响应时,发生错误。

Debugger showing values have successfully set in TestDto2 uuidList2 field

遵循@JAsgarov的建议,如果我重写UUIDListConvertor中的方法并自己传递正确的TypeReference,那么程序将按预期工作。

这是我在 UUIDListConvertor 中写的内容


@Converter
@Slf4j
public class JsonConverter<T> implements AttributeConverter<T, String> {
    private final TypeReference<T> typeReference = new TypeReference<T>() {};
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(T object) {
        log.info("converting to database column");
        try {
            return objectMapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Failed to serialize JSON data", e);
        }
    }

    @Override
    public T convertToEntityAttribute(String json) {
        log.info("converting to entity attribute");
        try {
            return objectMapper.readValue(json, typeReference);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Failed to deserialize JSON data", e);
        }
    }

但我无法理解为什么 JsonConvertor 中的 T 没有正确评估为

List<UUID>
。这是 TypeReference 在调试器中的样子

TypeReference in debugger

jackson spring-jdbc java-17 spring-boot-3
1个回答
0
投票

遵循@JAsgarov的建议,如果我重写

UUIDListConvertor
中的方法并自己传递正确的
TypeReference
,那么程序将按预期工作。问题似乎是 erasure 在 Java 中的工作方式,正如 @JAsgarov 在问题的评论部分所指出的。

这是我在

UUIDListConvertor
中写的内容。


@Converter
@Slf4j
public class JsonConverter<T> implements AttributeConverter<T, String> {
    private final TypeReference<T> typeReference = new TypeReference<T>() {};
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(T object) {
        log.info("converting to database column");
        try {
            return objectMapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Failed to serialize JSON data", e);
        }
    }

    @Override
    public T convertToEntityAttribute(String json) {
        log.info("converting to entity attribute");
        try {
            return objectMapper.readValue(json, typeReference);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Failed to deserialize JSON data", e);
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.