В этой статье я решил вернуться к основам языка Java и поговорить об исключениях. Они являются основной частью языка Java, но есть ли в них нечто большее, чем фундаментальные основы ловушек? Мы рассмотрим исключения с разных точек зрения и уровней работы с Java-приложениями, а также какие возможности у нас есть для их обработки.

Так почему новичок в профи? Что ж, эта статья представляет собой путешествие по собранию личных мыслей и выводов об исключениях. Я собираюсь представить их, поскольку я узнал о них со временем, и как я обнаружил их использование до сегодняшнего дня.

Мы рассмотрим исключения из базовой обработки и концепций, использования в веб-приложениях, проблемы совместимости и решения с потоковым API, а также функциональный подход к исключениям.

В использовании исключений нет никакого неправильного или правильного сценария, но я хотел бы, чтобы мы поняли несколько вариантов использования и какие возможности и свободы у вас есть при написании приложений Java.

Базовая обработка исключений

Давайте начнем с самого начала, вы только начинаете работать с Java, немного поиграетесь с основным методом и следуете некоторым руководствам по наследованию или университетскому заданию. Вы любите приключения и хотите попробовать что-то более сложное, поэтому допустим, вы решили читать вводимые данные вашего нового приложения из файла.

Но что у нас здесь?

Вероятно, впервые вы столкнулись с исключениями в контексте простой попытки чтения или записи файла на диск.

На этом этапе наиболее соблазнительно просто применить столь распространенный шаблон разработки, управляемой IDE (IDD), и позволить среде IDE написать код за вас, либо объявив его в подписи, либо добавив блок try-catch.

Но давайте остановимся и спросим себя ... а что же является исключением? Ну согласно документации Oracle исключение - это в первую очередь исключительное событие. После создания исключение распространяется через стек вызовов до тех пор, пока оно не достигнет обработчика (блока catch) или не завершит работу системы времени выполнения.

Чтобы немного углубиться в основы теории, есть два типа исключений: проверенные исключения и непроверенные (во время выполнения) исключения.

Проверенные исключения расширяют Exception class, и именно на них ваш компилятор будет жаловаться, как на скриншоте ранее. Существует цепочка жалоб или, так сказать, ограничений компилятора, которые:

  • Начинается с инструкции выбросить проверенное исключение (например, throw new IOException())
  • Продолжает, объявляя в сигнатуре вашего метода, что вы можете его выбросить (например, throws IOException)
  • Заканчивается тем, что он окружен блоком try-catch (например, catch(IOException e) {…}).

Цель проверенных исключений состоит в том, что разработчики библиотек API могут использовать их, чтобы сообщить вам, что (не) предсказуемое поведение может произойти при вызове их кода. Исходя из этого, компилятор заставляет вас быть готовым и предлагает свободу выполнения различных типов действий в зависимости от типа исключения.

По правде говоря, на практике в 90% проверенных случаев исключения вы не будете беспокоиться о специфике исключения, которое может быть сгенерировано методом, и будет перехватывать родительский класс Exception, регистрировать исключение и корректно завершать работу. В остальных 10% случаев у вас может быть какая-то логика повтора, или механизм отчетности, или запасной способ сделать процесс отказоустойчивым без необходимости вмешательства человека.

Что касается другого типа исключений, непроверенные исключения расширяют класс RuntimeException и должны быть ошибками программирования, которые не были предвидены разработчиками, такими как: исключение нулевого указателя (классическое), арифметические исключения (например, деление на 0), индекс массива вне пределов и другие.

Также нередко вручную генерировать исключения времени выполнения, но в этом случае компилятор не будет заставлять вас объявлять или обрабатывать исключение, как в случае с проверенными исключениями.

Единственная ошибка - это неправильное использование исключений, слишком много полагаясь на них для логики программирования, что сделало бы приложение очень трудным для отслеживания (аналогично предложениям goto в старых приложениях C), а также медленнее, поскольку механизм обработки исключений тяжелее, чем нормальное выполнение программы. один. Пока вы помните, что исключения должны быть исключительными, все должно быть в порядке.

Исключения и HTTP-ответы в веб-приложениях

В общем, проверенные исключения в конечном итоге приводят к блокам try-catch, которые, на мой взгляд, делают код менее читаемым, вкладывая его в блоки.

Но есть ли более приятный вариант использования исключений, который вместо этого сделает ваш код более читаемым и более легким для понимания?

Да, есть! Spring - одна из самых популярных сред Java, используемых в корпоративном мире, и Spring boot web нашел очень элегантный и полезный вариант использования исключений в веб-приложениях.

Если вы помните, если оставить необработанные исключения, они поднимутся по трассировке стека вызовов и в конечном итоге завершат ваш поток. Spring взял эту идею и применил ее на уровне HTTP-запросов. Очевидно, что получение исключения серверной части из-за HTTP-запроса не завершит работу сервера. Вместо этого по умолчанию он будет обрабатываться примитивно и приведет к HTTP-коду 500 (внутренняя ошибка сервера), а также к трассировке стека того, что пошло не так; но это редко то, что вам нужно!

Используя шаблон проектирования Proxy и концепцию Spring AOP (аспектно-ориентированное программирование), разработчики решили ввести функциональность, называемую советом контроллера. По сути, он сопоставляет разные типы исключений с разными кодами ошибок и сообщениями статуса HTTP. Таким образом вы пишете меньше кода.

Например, когда ресурс не найден, вы можете просто создать специальное исключение, которое является репрезентативным для кода HTTP 404, а также для конкретного сообщения.

Как эта функция, вероятно, работает в Spring, или, по крайней мере, как я реализовал ее в структуре веб-API, в которой не было рекомендаций контроллера Spring, заключалось в использовании аспектов для добавления pointcuts к методам API, перехвата вызовов с помощью рекомендаций до и после , и попробуйте поймать точку соединения, чтобы сопоставить любые исключения с требуемым ответом.

Советы по контроллеру предлагают мощный способ написания читаемого и простого в обслуживании кода. Вместо того, чтобы создавать пользовательскую ResponseEntity логику, вы сохраняете свет и уровень контроллера, и уровень сервиса и сосредотачиваетесь исключительно на логике домена, в то время как советы контроллера включают логику приложения, отвечающую за отображение различных кодов состояния ошибки на основе желаемого API.

Исключения и Stream API

Одна из неприятных вещей в потоках Java 8 заключается в том, что Java не спроектирована как функциональный язык программирования, а потоковый API был разработан для функционального программирования и отдает предпочтение чистым функциям. Однако функции и методы, которые генерируют исключения, не являются чистыми функциями, потому что возможность генерирования исключения означает, что они не предлагают ссылочную прозрачность, а генерирование исключения не является возвращаемым значением.

Итак, на практике проблема в том, что вы получаете этот тип ошибок компиляции.

Так что давайте снова вернемся к «разработке, управляемой IDE», не так ли?

Ужасно ... Есть причина, по которой лямбда-выражения, передаваемые в методы потокового API, должны иметь только одну строку, и я надеюсь, что всем очевидно, почему из приведенного выше примера кода.

Но послушайте, всегда есть другой стандартный вариант, давайте добавим его в сигнатуру метода.

Чего ждать? Но я рассмотрел оба варианта, что мне теперь делать? Почему не работает, тут же объявляю исключение! Если вы посмотрите на сообщение об ошибке, в нем говорится, что у вас есть необработанный IOException, но это не настоящая причина.

Причина, по которой приведенный выше код не компилируется, связана с сигнатурой метода функционального интерфейса Function. Если мы посмотрим, какой параметр принимает функция map в потоковом API, это будет Function<? super T, ? extends R>, а Function имеет следующую подпись интерфейса.

Однако, если вы подумаете о том, какой параметр вы пытаетесь передать потоку, метод map на самом деле больше похож на эти строки.

Вы даже не можете сказать, что ExceptionalFunction может даже расширить Function интерфейс, присутствующий в JDK, потому что правила переопределения будут нарушены в дочернем интерфейсе из-за объявления Exception в подписи. Таким образом, вы никогда не сможете передать ExceptionalFunction методу map.

В любом случае, возвращаясь к проблеме, быстрый, но не обслуживаемый способ преодоления этой практической проблемы функционального программирования, не одобряющей методы, которые генерируют исключения, состоит в том, чтобы извлечь приведенный выше пример try-catch в метод, такой, что код становится читаемым. Я, вероятно, не должен давать вам такие идеи, как эта. Проблема такого подхода в том, что вы не столкнетесь с подобными проблемами только один раз за весь проект. Это довольно частое явление, поэтому вам придется создать множество таких методов, как приведенный ниже (не делайте этого - в этом нет ничего безопасного!).

Более разумным решением будет использовать универсальные шаблоны и вместо того, чтобы копировать метод из приведенного выше примера в 50 различных местах и ​​способами, просто напишите его один раз в универсальном и многоразовом виде как для себя, так и для своих коллег. Подход здесь состоит в том, чтобы преобразовать то, что вам не нужно (например, ExceptionalFunction), в Function, как показано ниже, и внезапно больше не будет необработанной ошибки исключения.

Это довольно хороший пример того, как использовать универсальные шаблоны и функциональные интерфейсы для написания аккуратного многоразового кода.

FYI-функции, подобные приведенной выше, которые принимают функции в качестве параметров или возвращают функции, называются функциями высшего порядка.

Но не думаете ли вы, что работа с методами, которые генерируют исключения в потоковом API, является очень распространенной проблемой? Насколько вероятно, что вы первым решили ее?

Аааа, конечно, уже решена! Если вы не возражаете добавить дополнительную зависимость к вашему pom файлу зависимостей, вы можете найти эту функцию в библиотеке Jool с помощью Unchecked.function, а также с помощью аннотации Lombok @SneakyThrows.

Функциональный подход к исключениям

Однако, возвращаясь к принципам функционального программирования, было бы неплохо иметь более чистый способ обработки этих исключений. В конце концов, решения, которые я выделил в предыдущем разделе, либо подавляют исключение и регистрируют его, либо могут повторно генерировать исключение как исключение времени выполнения. Это скорее быстрый способ избавиться от исключения.

Разработчики, которые работали с более функциональными языками программирования, такими как Scala, знают, что есть лучший способ обработки исключений - использование Try объектов.

Проще говоря, объект Try в Scala обертывает вычисление, которое может привести к исключению. Это похоже на объект Optional в Java, только вместо Some или None это либо Success, либо Failure. Как и Optional, он поддерживает общие методы, также присутствующие в API потока, такие как map, flatmap, filter, get и другие.

В более формальных терминах объект Try - это тип монадический контейнер. Он инкапсулирует функцию более высокого порядка, превращая ее в монадическое значение, и предлагает серию функций, называемых монадическими функциями, которые выводят другие монадические значения. Это позволяет использовать декларативный API типа конвейера / цепочки, который нам нравится.

Хорошо, но как это нам помогает, и что ... вы хотите сказать мне, что Scala лучше Java?

Ну, я не об этом пытался сказать. Спасибо разработчикам Vavr, Java-фреймворка, который я большой поклонник, у нас есть эта функция, как и многие другие, прямо из коробки! Vavr поддерживает объекты try в Java и, следовательно, более чистый способ обработки исключений.

Помните старый классический вопрос на собеседовании: «Будет ли блок finally все еще выполняться, если вы вернетесь внутрь блока try-catch»? Ну кого это волнует, есть гораздо лучший способ обрабатывать исключения без необходимости знать их подводные камни и внутренние императивы, и это использовать объекты try.

Я попытался построить более сложный пример, чтобы выделить эту особенность, надеюсь, она меня поймет. Тем не менее, я еще не сталкивался с ситуацией, когда нельзя использовать Try объекты и приходится полагаться на традиционные / ручные блоки try-catch.

Сценарии могут быть намного проще, без каких-либо recover или match шаблонов. Вы можете просто обернуть вызов с помощью Try, а затем обработать его с помощью map, filter, get, getOrElse и т. Д.

Давайте представим вариант использования, в котором вы не хотите обрабатывать Try или исключение в том же методе, в котором оно произошло, как обычно, добавляя throws к сигнатуре метода и блоку try-catch на несколько стеков вызовов вверх.

Но разве не было бы намного более интуитивно понятным и гибким, если бы ваш метод возвращал Try вместо объявления throws? Таким образом, вы знаете, что метод мог не выполняться успешно, независимо от того, что он вернет результат попытки, и в случае Failure вы можете передать его компоненту обработчика исключений, используя композицию (просто играя с мыслями здесь), а не принудительно обрабатывать его только где-нибудь в трассировке стека вызовов.

Чтобы закрыть тему, я советую немного подумать об этой функциональности, если вы новичок в ней, ознакомиться с концепцией и непредвзято подумать, как это может быть полезно в ваших проектах.

Заключение

Подводя итоги этой статьи, как Java, так и окружающие ее фреймворки прошли долгий путь, и исключения также претерпели изменения. Эта статья предназначена как тизер, чтобы побудить вас изучить эту тему больше. Например, мы вообще не говорили о вариантах обработки исключений с помощью Flux API ядра реактора, и это тоже интересно и полно.

Конечно, в повседневной работе нам все еще нужно взаимодействовать либо с устаревшим кодом, либо с библиотеками, которые объявляют проверенные исключения, но это не значит, что нам нужно писать код, как мы это делали 10 лет назад. Сегодня у нас есть варианты (и попытки)!

Я надеюсь, что вы нашли эту статью полезной, и, пожалуйста, поделитесь комментарием ниже!