diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java index 6ec743b44..115b70c6b 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java @@ -17,11 +17,14 @@ package org.springframework.cloud.openfeign.support; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import feign.QueryMapEncoder; import feign.querymap.BeanQueryMapEncoder; +import feign.querymap.FieldQueryMapEncoder; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -37,6 +40,12 @@ */ public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder { + /** + * QueryMapEncoder for Java Record types. Uses field-based reflection to encode Record + * components as query parameters. + */ + private final QueryMapEncoder recordEncoder = new FieldQueryMapEncoder(); + /** * Page index parameter name. */ @@ -71,6 +80,13 @@ public void setSortParameter(String sortParameter) { @Override public Map encode(Object object) { + // Null safety: return empty map for null input + if (object == null) { + return Collections.emptyMap(); + } + + // Priority 1: Pageable handling (existing logic) + // Must be checked before Record to avoid serialVersionUID conflicts if (supports(object)) { Map queryMap = new HashMap<>(); @@ -90,9 +106,15 @@ else if (object instanceof Sort sort) { } return queryMap; } - else { - return super.encode(object); + + // Priority 2: Java Record handling (new logic) + // Records require field-based reflection instead of getter methods + if (object.getClass().isRecord()) { + return recordEncoder.encode(object); } + + // Priority 3: Bean/POJO handling (existing fallback) + return super.encode(object); } private void applySort(Map queryMap, Sort sort) { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoderTests.java new file mode 100644 index 000000000..9411e3722 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/RecordQueryMapEncoderTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.openfeign.support; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for Java Record support in {@link PageableSpringQueryMapEncoder}. + * + * @author Joo (Seongho) + */ +class RecordQueryMapEncoderTests { + + private final PageableSpringQueryMapEncoder encoder = new PageableSpringQueryMapEncoder(); + + @Test + void testSimpleRecordEncoding() { + // TC1: Record Only + SimpleRecord record = new SimpleRecord("hello", 1); + + Map result = encoder.encode(record); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(2); + assertThat(result.get("keyword")).isEqualTo("hello"); + assertThat(result.get("page")).isEqualTo(1); + } + + @Test + void testEmptyRecordEncoding() { + // TC4: Empty Record + EmptyRecord record = new EmptyRecord(); + + Map result = encoder.encode(record); + + assertThat(result).isNotNull(); + assertThat(result).isEmpty(); + } + + @Test + void testRecordWithNullField() { + // TC5: Record with null field + RecordWithNull record = new RecordWithNull(null); + + Map result = encoder.encode(record); + + assertThat(result).isNotNull(); + // FieldQueryMapEncoder excludes null fields + assertThat(result).isEmpty(); + } + + @Test + void testNullInput() { + // TC6: Null input + Map result = encoder.encode(null); + + assertThat(result).isNotNull(); + assertThat(result).isEmpty(); + } + + @Test + void testPageableEncodingNoConflict() { + // TC2 & TC7: Pageable encoding - ensure no serialVersionUID conflict + Pageable pageable = PageRequest.of(2, 20, Sort.by("name").ascending()); + + Map result = encoder.encode(pageable); + + assertThat(result).isNotNull(); + assertThat(result).containsEntry("page", 2); + assertThat(result).containsEntry("size", 20); + assertThat(result).containsKey("sort"); + // Most important: no IllegalStateException with "Duplicate key serialVersionUID" + } + + @Test + void testSortEncoding() { + // Sort encoding test + Sort sort = Sort.by("name", "age").ascending(); + + Map result = encoder.encode(sort); + + assertThat(result).isNotNull(); + assertThat(result).containsKey("sort"); + } + + @Test + void testPojoFallback() { + // TC3: Bean/POJO fallback to BeanQueryMapEncoder + SimplePojo pojo = new SimplePojo("John", 30); + + Map result = encoder.encode(pojo); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(2); + assertThat(result.get("name")).isEqualTo("John"); + assertThat(result.get("age")).isEqualTo(30); + } + + // Test Records (local definitions) + record SimpleRecord(String keyword, int page) { + } + + record EmptyRecord() { + } + + record RecordWithNull(String value) { + } + + // Test POJO for fallback testing + public static class SimplePojo { + + private final String name; + + private final int age; + + SimplePojo(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + } + +}