Сериализация класса в query string
Есть задача сериализации кастомного класса в query string (параметры в URL строке). Какие-то поля класса должны быть обязательно, какие-то не обязательно. Например:
public class UserQueryParameters {
String name; // *
int age; // Не обязательно
String sex; // Не обязательно
}
Надо как-то объявить, какие поля сериализовать (в реалиях java это маршаллинг называется, вроде), какие нет, а затем преобразовывать получившуюся структуру в строку вида
name=John&age=23
Как это можно реализовать?
Пробовал с использованием lombok аннотаций:
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class UserQueryParameters {
String name; // *
int age; // Не обязательно
String sex; // Не обязательно
}
Далее через билдер формирование инстанса класса с заполненными полями:
UserQueryParameters params = UserQueryParameters.builder()
.name("John")
.sex("m")
.build();
И формирование query string путём рефлексии:
import java.lang.reflect.Field;
// ...
for (Field field : params.getClass().getDeclaredFields()) {
if (field.get(params) != null) {
addToQueryString(field.get(params));
}
}
Но, очевидно, такое решение не подходит, т.к. для int поля, например, будет не null, а 0.
Ответы (2 шт):
Поскольку вы обрабатываете строковый запрос. Я бы сделал сначала сериализацию в промежуточный класс со строковыми полями. Чтобы не получить необработанную ошибку на конвертации.
public class ResuestUserQueryParameters {
String name;
String age;
String sex;
}
А уже далее обрабатывал его, проверка на реквайред и прочие значения.
А поскольку обычно сами реквесты уже по умолчанию сделают это за вас. (распарсят запрос на строки). Вам по сути нужно самому проверить и обработать параметры
Собственно, ваш вопрос содержит половину ответа :)
Надо как-то объявить, какие поля сериализовать (в реалиях java это маршаллинг называется, вроде), какие нет
Для этого в java используется механизм аннотаций. Вам нужно создать собственные аннотации, пометить ими поля в классах, и затем в маршаллере пройтись reflection-ом по полям объекта класса, выбрать из них помеченные аннотацией, и сгенерировать из помеченных полей пары key=value для запроса.
Я сделал простенький пример. Создал аннотацию QueryField с двумя параметрами
requiredопределяет, обязательное поле или нет. По умолчаниюtrue- это сделано для того, чтобы явным образом обозначать необязательные поля.nameзадаёт, какое имя использовать при сериализации. Если не задано, то используется имя поля.
Вот пример, как пользоваться аннотацией. Класс с полями age, name и sex. Обязательно только поле name, поле sex сериализуется как gender.
public static class TestData {
@QueryField
public String name; // *
@QueryField(required = false)
public Integer age; // Не обязательно
@QueryField(name = "gender", required = false)
public String sex; // Не обязательно
}
Аннотация в java определяется ключевым словом @interface:
package org.example.query;
import java.lang.annotation.*;
// Keep annotation in generated byte code
@Retention(RetentionPolicy.RUNTIME)
// Compiler hint: the annotation should be applied to fields only
@Target(ElementType.FIELD)
public @interface QueryField {
// Custom field name in the generated query string
public String name() default "";
// If `true` the field value must be non-null
public boolean required() default true;
}
(подробнее в Java Language Specification)
Теперь можно было бы перейти к написанию сериализатора, но, как поклонник Test Driven Development, я начну с теста. Он великоват для вставки сюда, поэтому я выложил его в pastebin.
Поясню лишь идею теста.
- Подготовить данные.
- Сгенерировать строку.
- Так как порядок полей в строке, вообще говоря, не определён, то вместо сравнения с эталонной строкой лучше строку разобрать и сложить значения в контейнер.
- Потом в тесте проверить, какие именно значения обнаружились в контейнере.
- Заодно проверить, что ничего лишего там не завелось.
// Тест для сериализатора с одним отсуствующим необязательным полем.
@Test
void test_serialize() throws SerializerException {
TestData sample = new TestData();
sample.age = 23;
sample.name = "John";
String actual = Serializer.buildQuery(sample);
Map<String, String> data = parseQuery(actual);
assertTrue(data.containsKey("age"));
assertTrue(data.containsKey("name"));
// Проверяем, что других полей нет.
assertEquals(2, data.keySet().size());
assertEquals("John", data.get("name"));
assertEquals("23", data.get("age"));
}
Теперь, когда тест готов, можно написать простенький сериализатор для иллюстрации идеи.
Там всё просто. Сначала перебираем поля объекта и ищем поля с аннотацией QueryField. Если такое поле найдено, пытаемся извлечь поле. В ответ JRE может выкинуть исключения - обрабатываем.
Затем проверяем, вдруг поле пустое и одновременно необязательное. Такие поля пропускаем. Про пустое обязательное поле кидаем исключение.
Затем конвертируем значение в строку (на всяк пожарный все опасные символы экранируем) и подбираем имя - либо заданное в аннотации, либо полученное из имени поля. Добавляем пару "name=valueText" в строку результата. Профит!
public static String buildQuery(Object obj) throws SerializerException {
StringBuilder result = new StringBuilder();
for (java.lang.reflect.Field fld : obj.getClass().getFields()) {
QueryField annot = fld.getAnnotation(QueryField.class);
if (annot == null) {
// This field has no "QueryField" annotation, ignore it.
continue;
}
Object fldVal;
try {
fldVal = fld.get(obj);
} catch (IllegalArgumentException e) {
throw new RuntimeException("Should never happen", e);
} catch (IllegalAccessException e) {
throw new SerializerException("Failed to access field "+fld.getName(), e);
}
if (fldVal == null) {
if (annot.required()) {
throw new SerializerException("Required field is null");
} else {
// Ignore optional fields
continue;
}
}
String valText = urlEncode(fldVal.toString());
String valName = annot.name();
if (valName.length() == 0) {
// No custom name, use field name instead
valName = fld.getName();
}
valName = urlEncode(valName);
result.append('&').append(valName).append('=').append(valText);
}
return result.substring(1);
}
Как-то так оно делается.