Введение
В эпоху разделения клиентской и серверной частей наша жизнь полна бесчисленных возможностей и трудностей — в том числе внезапно возникающих багов, которые сводят с ума. Сегодня мы поговорим о классической проблеме, из-за которой бесчисленные разработчики не знают, смеяться им или плакать:Серверная часть Long Слишком длинный ID типа приводит к потере точности на клиентской стороне。Если говорить об этой проблеме, то это и правда «корень всех зол», ведь JavaScript может безопасно обрабатывать только Number.MAX_SAFE_INTEGER А как насчёт чисел в пределах этого значения(то есть 9007199254740991)?
Если вы когда-нибудь ломали голову над этой проблемой, не переживайте: вы не одиноки. Сегодня мы не только с юмором разберём эту “задачу века”, но и поможем устранить её первопричину, чтобы ваш код больше не “терял точность”.
Контекст проблемы:почему Long Так долго?
Прежде всего, нам нужно начать с Long начнём с типов.Long,то есть Java целочисленный тип разрядностью 64 бита, для тех, кто любит обрабатывать Большие данные、для Java с её большими числами этот тип — настоящее спасение. Однако мир фронтенда немного «ограничен в познаниях»:он умеет работать только с целыми числами в пределах 53 бит. Да, вы не ослышались, в этом отношении JavaScript Словно старый учёный, который вечно «запаздывает на полшага», при встрече с большими числами он начинает возиться с десятичными разделителями и показателями степени, а в итоге выдаёт число, от которого хочется впасть в отчаяние. И вот проблема потери точности, словно призрак, начинает бродить по вашему проекту.
Первая встреча:истории о потере точности
История началась однажды с сообщения об ошибке:“Братец-разработчик, посмотри, почему этот ID изменился? Это же не тот, который я База данных тот самый ID оттуда!» Вы хмуритесь и смотрите: да,9223372036854775807 превратилось в 9223372036854776000,ну надо же, эти лишние цифры появились словно по волшебству. Ведь серверная часть передала всё правильно! Вы чешете затылок и думаете:“Это точно вина фронтенда!”
Кто виноват: фронтенд или бэкенд?——Докапываемся до первопричины
Вообще-то сваливать эту проблему на фронтенд не совсем уж безосновательно. Давайте посмотрим JavaScript «Слабое место» при работе с числами.JavaScript принадлежащий Number Тип использует формат чисел с плавающей запятой двойной точности по стандарту IEEE 754 и способен безопасно представлять лишь 53 двоичных разряда, то есть Number.MAX_SAFE_INTEGER значение — 9007199254740991.
Другими словами, целые числа, выходящие за этот диапазон,JavaScript начнёт «терять точность», и его «мозговой ёмкости» внезапно окажется недостаточно. Тогда ваши Long типа ID превращаются для него в набор не столь «сексуальных» числовых значений. Именно поэтому при обработке этих неправдоподобно длинных ID фронтенд вынужден пойти на некоторую «жертву»: ваш прежде верный ID обрезается и превращается в новое «занятное» число.
От потрясения к спокойствию: в поисках решения
Раз корень проблемы найден, пришло время нам, разработчикам, показать себя. Далее я подробно разберу несколько решений и расскажу о достоинствах и недостатках каждого — ведь все дороги ведут в Рим.
1. Прямое преобразование в строку: просто и грубо, зато эффективно
Столкнувшись с такой проблемой, мы наверняка первым делом подумаем о самом простом и грубом способе: напрямую Long типа Преобразование данных превратить в строку — и дело с концом! Раз уж JavaScript — фанат чисел с плавающей точкой, то давайте просто вернём ему задачу и скажем: «Тебе нужно лишь считать это строкой, не беспокойся о её длине!»
Как этого добиться? На самом деле всё просто: используйте Jackson предоставленный ToStringSerializer, мы можем легко Long Преобразовать в строку.
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
public class UserDto {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
// 其他字段
}Таким образом, когда серверная часть возвращает UserDto в то время,Long типа id будут сериализованы в строки, и интерфейс тоже получит строки — проблема точности решится сама собой. Словно на голову JavaScript приклеили записку:「Это строка, можешь не беспокоиться!」
2. На фронтенде используется BigInt: точные вычисления даже с большими числами
Конечно, простой и прямолинейный подход подходит не для всех случаев. Представьте, что клиентской части нужно выполнить какие-то математические операции с этим ID:простое преобразование в строку окажется не лучшим решением. Как же клиентской части обрабатывать такие «сверхдлинные» ID?
К счастью,JavaScript тоже имеет свои достоинства. Было введено BigInt После этого,JavaScript Наконец-то он перестал быть тем занудой, который только и умеет, что возиться с десятичными запятыми.BigInt — новый примитивный тип данных, специально предназначенный для работы с целыми числами произвольной точности. Вы можете сделать так:
const id = BigInt("9223372036854775807");
console.log(id + 1n); // 输出:9223372036854775808nТаким образом, на клиентской стороне вы сможете точно обрабатывать Long данные этого типа, чтобы избежать потери точности. Конечно, здесь есть небольшое замечание:BigInt Это поддерживают не все браузеры, поэтому нужно убедиться в совместимости среды, в которой работает ваше приложение.
3. Пользовательская сериализация:упрощаем сложные задачи
Хотя описанные выше способы уже позволяют решить большинство проблем, иногда встречаются ситуации, требующие более тонкого контроля. В таких случаях, Jackson — здесь и пригодится пользовательский сериализатор.
Мы можем написать собственный сериализатор, чтобы гибко управлять этим в соответствии с требованиями Long процесс сериализации поля типа. Например, при сериализации мы можем решить, следует ли Long Преобразовать в строку либо обрабатывать по-разному в зависимости от конкретных условий:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class CustomLongSerializer extends JsonSerializer<Long> {
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value != null) {
gen.writeString(value.toString());
}
}
}Затем примените этот пользовательский сериализатор там, где требуется:
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public class UserDto {
@JsonSerialize(using = CustomLongSerializer.class)
private Long id;
// 其他字段
}Так вы сможете контролировать весь процесс сериализации, обеспечивая, чтобы каждый Long всё можно обработать так, как вам нужно. Конечно, этот способ гибкий, но несколько более сложный: потребуется написать несколько дополнительных строк кода, что также может увеличить затраты на сопровождение.
4. Глобальная обработка:решение, экономящее время и силы
Если вы хотите решить эту проблему глобально и избавить себя от необходимости настраивать аннотации для каждого поля, можно рассмотреть глобальную настройку ObjectMapper。Путём Spring Boot настроить глобальный в ObjectMapper,вы можете позволить всем Long Все поля этого типа автоматически преобразуются в строки。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
mapper.registerModule(module);
return mapper;
}
}Одно решение раз и навсегда:не нужно бояться забыть настроить сериализатор для какого-либо поля, и весь проект может наслаждаться работой без потери точности. Конечно, при использовании глобальной конфигурации нужно учитывать, что она может затронуть некоторые значения, которые вы не хотели бы преобразовывать Long поле, поэтому этот вопрос требует осторожного рассмотрения.
Практический пример: извилистый путь к исправлению бага
Чтобы вы лучше поняли, как эти решения работают на практике, расскажу историю из реального проекта. Проект касался управления идентификаторами пользователей, и из-за большого числа пользователей на серверной стороне использовали Long уникальный идентификатор этого типа. Не прошло и нескольких дней после запуска проекта, как пользователи начали писать:“Почему мой ID стал выглядеть иначе?”.
Первый шаг:поиск первопричины проблемы
Сначала мы проверили код фронтенда и обнаружили, что JavaScript принадлежащий Number тип действительно не может точно представить это число длиной 19 цифр, что и привело к потере точности. Затем мы изучили серверный код и обнаружили, что, хотя Long данные этого типа корректны на стороне сервера, но при передаче через REST API При возврате данных во фронтенд точность числа была потеряна.
Шаг второй: выбрать подходящее решение
Чтобы быстро решить проблему, мы решили сначала использовать @JsonSerialize(using = ToStringSerializer.class) этот простой и эффективный способ. Благодаря ему нам удалось избежать получения клиентской частью чисел с потерей точности.
Однако проблема не была полностью решена. В последующих требованиях возникла необходимость выполнять на стороне клиента определённые операции с ID, например сортировать ID пользователей. Тут строки уже оказались не вполне подходящим решением.
Шаг третий: использование BigInt Решение проблем вычислений на фронтенде
Поэтому мы решили внедрить на фронтенде BigInt。Используя BigInt,клиентская часть может не только точно хранить эти сверхдлинные ID, но и выполнять необходимые математические операции. Тестирование показало, что этот способ хорошо работает во всех основных современных браузерах; единственный недостаток — не слишком хорошая поддержка некоторых старых версий браузеров.
Шаг четвёртый:окончательная глобальная конфигурация
Чтобы подобные проблемы не повторялись в будущем, мы решили ToStringSerializer Настроить для глобального применения. Это позволит всем Long данные этого типа при сериализации автоматически преобразуются в строки, что обеспечивает точность данных на стороне клиента и сокращает повторяющиеся настройки в коде.
Заключение: конец потере точности
Из этого примера видно, что хотя Long Проблема потери точности этого типа кажется простой,но в реальных проектах может таить немало рисков。Сравнив и опробовав несколько решений,мы в итоге нашли вариант,лучше всего подходящий нашему проекту。
Надеюсь, эта юмористическая и подробная статья поможет вам при работе с Long при возникновении проблем с потерей точности типов избежать лишних ошибок. Если у вас был похожий опыт, поделитесь своей историей в комментариях: возможно, она подскажет другим разработчикам новые идеи.
И напоследок помните: код подобен жизни, случайные «потери» не страшны, главное — найти подходящий «сериализатор», который вернёт всё в нужное русло. Желаю, чтобы ваш код больше никогда не «терял точность», оставался точным и работал без помех!