Пожалуйста, прекратите использовать типы enum во внешних интерфейсах!

Java

GitHub 18k Star Путь Java-инженера к тому, чтобы стать богом, не приходите и не узнайте об этом!

Путь Java-инженера GitHub 18k Star к тому, чтобы стать богом, разве вы не хотите узнать об этом!

GitHub 18k Star Путь инженера Java к тому, чтобы стать богом, на самом деле не приходите, чтобы узнать об этом!

Недавно в нашей онлайн-среде возникла проблема.Во время выполнения онлайн-кода было выброшено исключение 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» есть аналогичные соглашения:

-w1538

Здесь указано «Для параметров сторонней библиотеки можно использовать перечисление, но возвращаемое значение использовать перечисление запрещено". Идея, стоящая за этим, - это то, что было упомянуто выше в этой статье.

Расширение мышления

Почему в параметрах могут быть перечисления?

Не знаю, задумывались ли вы над этой проблемой, но на самом деле это как-то связано с обязанностями сторонней библиотеки.

При нормальных обстоятельствах, когда система 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], оставьте мне сообщение прямо в фоновом режиме.