GitHub 18k Star Путь Java-инженера к тому, чтобы стать богом, не приходите и не узнайте об этом!
Путь Java-инженера GitHub 18k Star к тому, чтобы стать богом, разве вы не хотите узнать об этом!
Недавно в нашей онлайн-среде возникла проблема.Во время выполнения онлайн-кода было выброшено исключение IllegalArgumentException.После анализа стека было обнаружено, что наиболее фундаментальным исключением было следующее:
java.lang.IllegalArgumentException:
No enum constant com.a.b.f.m.a.c.AType.P_M
Вероятно, вышеприведенное содержимое кажется очень простым, выдается сообщение об ошибке, что элемент перечисления P_M не найден в классе перечисления AType.
Итак, после расследования мы обнаружили, что до того, как исключение начало появляться в сети, была выпущена нижестоящая система, на которую опирается приложение, и в процессе выпуска изменился пакет API, и основное изменение было в классе возвращаемого значения ответа. Интерфейс RPC Один из параметров перечисления в AType добавляет элемент перечисления P_M.
Однако, когда нижестоящая система была выпущена, система, за которую мы отвечали, не была уведомлена об обновлении, поэтому было сообщено об ошибке.
Давайте проанализируем, почему это происходит.
Проблема воспроизводится
Во-первых, возвращаемое значение интерфейса сторонней библиотеки, предоставленной нижестоящей системой А, имеет тип параметра, который является типом перечисления.
Односторонняя библиотека относится к зависимостям в этом проекте. Сторонняя библиотека относится к зависимостям, предоставленным другими проектами в компании. Сторонняя библиотека относится к зависимостям от третьих сторон, таких как другие организации и компании.
public interface AFacadeService {
public AResponse doSth(ARequest aRequest);
}
public Class AResponse{
private Boolean success;
private AType aType;
}
public enum AType{
P_T,
A_B
}
Затем система B использует эту стороннюю библиотеку и будет вызывать метод doSth службы AFacadeService через удаленный вызов RPC.
public class BService {
@Autowired
AFacadeService aFacadeService;
public void doSth(){
ARequest aRequest = new ARequest();
AResponse aResponse = aFacadeService.doSth(aRequest);
AType aType = aResponse.getAType();
}
}
В настоящее время, если обе системы A и B полагаются на одну и ту же стороннюю библиотеку, перечисление AType, используемое ими, будет одним и тем же классом, и элементы перечисления в нем будут одинаковыми. нет в чем проблема.
Однако, если в один прекрасный день сторонняя библиотека будет обновлена, и в класс перечисления AType будет добавлен новый элемент перечисления P_M.В настоящее время обновлена только система A, но система B не обновлена.
Тогда AType, от которого зависит система A, выглядит следующим образом:
public enum AType{
P_T,
A_B,
P_M
}
AType, от которого зависит система B, выглядит следующим образом:
public enum AType{
P_T,
A_B
}
В этом случае, когда система B вызывает систему A через RPC, если бит типа aType в ответе AResponse, возвращаемом системой A, добавляет P_M, система B не сможет его проанализировать. Обычно в это время в инфраструктуре RPC возникает исключение десериализации. привести к прерыванию программы. **
Принципиальный анализ
Мы четко проанализировали явление этой проблемы, затем давайте рассмотрим принцип и почему возникает такая аномалия.
На самом деле этот принцип не сложен, такойБольшинство фреймворков RPC будут использовать формат JSON для передачи данных., то есть клиент сериализует возвращаемое значение в строку JSON, а сервер десериализует строку JSON в объект Java.
В процессе десериализации JSON для типа перечисления он попытается вызвать метод valueOf соответствующего класса перечисления, чтобы получить соответствующее перечисление.
И когда мы смотрим на реализацию метода valueOf класса перечисления, мы можем обнаружить, чтоIllegalArgumentException выдается, если соответствующий элемент перечисления не найден в классе перечисления.:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
Что касается этого вопроса, на самом деле в «Руководстве по разработке Java для Alibaba» есть аналогичные соглашения:

Здесь указано «Для параметров сторонней библиотеки можно использовать перечисление, но возвращаемое значение использовать перечисление запрещено". Идея, стоящая за этим, - это то, что было упомянуто выше в этой статье.
Расширение мышления
Почему в параметрах могут быть перечисления?
Не знаю, задумывались ли вы над этой проблемой, но на самом деле это как-то связано с обязанностями сторонней библиотеки.
При нормальных обстоятельствах, когда система A хочет предоставить удаленный интерфейс для других вызовов, она определяет стороннюю библиотеку, чтобы сообщить вызывающей стороне, как создавать параметры и какой интерфейс вызывать.
Вызывающая сторона этой сторонней библиотеки будет вызывать в соответствии с определенным в ней содержимым. Процесс построения параметров завершается системой B. Если система B использует старую стороннюю библиотеку, используемое перечисление, естественно, является существующим, а новые использоваться не будут, так что это тоже не будет проблемой. .
Например, в предыдущем примере, когда система B вызывает систему A и использует AType при построении параметров, есть только две опции, P_T и A_B.Хотя система A уже поддерживает P_M, система B ее не использует.
Если система B хочет использовать P_M, ей необходимо обновить стороннюю библиотеку.
Однако возвращаемое значение отличается. Возвращаемое значение не контролируется клиентом. То, что возвращает сервер, определяется сторонней библиотекой, на которую он опирается.
Однако на самом деле, по сравнению с положениями руководства,Я больше склонен не использовать перечисления для входных и выходных параметров в интерфейсах RPC.
Как правило, есть несколько соображений, когда мы хотим использовать перечисления:
-
1. Перечисление строго контролирует входящее содержимое нижестоящей системы, чтобы избежать недопустимых символов.
-
2. Нижестоящей системе удобно знать, какие значения можно передавать, и ошибиться нелегко.
Следует признать, что использование перечислений имеет некоторые преимущества, но я не рекомендую его по следующим причинам:
-
1. При обновлении сторонней библиотеки и удалении некоторых элементов перечисления в перечислении также возникнет проблема с использованием перечисления во входном параметре, и вызывающая сторона не сможет распознать элемент перечисления.
-
2. Иногда существует несколько восходящих и нисходящих систем, например, система C косвенно вызывает систему A через систему B. Параметры системы A передаются из системы C, а система B выполняет только преобразование параметров. и сборка. В этом случае после обновления сторонней библиотеки системы A обе библиотеки B и C должны быть обновлены одновременно, и ни одна из них не будет совместима, если они не будут обновлены.
Я действительно рекомендую вам использовать строки вместо перечислений в интерфейсах., по сравнению с сильным типом перечисления строка является слабым типом.
Если перечисление в RPC-интерфейсе заменить строкой, двух упомянутых выше проблем можно избежать: вышестоящей системе нужно передать только строку, а законность конкретного значения должна быть только в системе A. Просто проверьте это из.
Для удобства вызывающего объекта можно использовать аннотацию @see javadoc, чтобы указать, что значение этого строкового поля получено из этого перечисления.
public Class AResponse{
private Boolean success;
/**
* @see AType
*/
private String aType;
}
Для относительно крупной интернет-компании, такой как Ali,Интерфейс предоставляется небрежно, абонентов могут быть сотни, и обновления интерфейса также являются нормой.Мы просто не можем просить всех вызывающих абонентов обновиться после каждого обновления сторонних библиотек., это совершенно нереально, а для некоторых звонящих он не будет использовать новые возможности, да и обновляться вообще не нужно.
Есть еще одна ситуация, которая выглядит особенной, но на самом деле встречается чаще, то есть иногда в пакете A объявлен интерфейс, а в пакете B определены некоторые константы enum. заказ разделен на множество уровней, каждый раз, когда вводится пакет, необходимо вводить десятки пакетов.
Что касается вызывающей стороны, я определенно не хочу, чтобы моя система вводила слишком много зависимостей,С одной стороны, слишком много зависимостей сделает процесс компиляции приложения очень медленным, и легко вызвать конфликты зависимостей.
Поэтому при вызове нижестоящего интерфейса, если тип поля в параметре перечисление, то мне ничего не остается, как полагаться на его стороннюю библиотеку. Но если это не перечисление, а просто строка, то я могу не зависеть.
Поэтому, когда мы определяем интерфейс, мы постараемся избежать использования строгого типа перечисления. В спецификации указано, что его нельзя использовать в возвращаемом значении, да и сам я предъявляю более высокие требования, то есть редко использую его даже во входных параметрах интерфейса.
Наконец, я просто не рекомендую использовать перечисление во входных и выходных параметрах внешних интерфейсов, я не имею в виду вообще не использовать перечисление, я уже упоминал во многих статьях, что перечисление имеет много преимуществ, и я часто использую это в моем коде. Итак, не отказывайтесь от еды из-за удушья.
Конечно, мнения в этой статье представляют только меня лично.Применимо ли это к другим людям, другим сценариям или практике других компаний, читатели должны различать сами.Рекомендуется больше думать об этом при ее использовании.
Об авторе:Hollis, человек с уникальным увлечением программированием, технический эксперт Alibaba, соавтор «Трех курсов для программистов» и автор серии статей «Дорога к Java-инженерам».
Если у вас есть комментарии, предложения или вы хотите пообщаться с автором, вы можете подписаться на официальный аккаунт [Hollis], оставьте мне сообщение прямо в фоновом режиме.