Progress28.ru

IT Новости
1 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Java exception in constructor

Is it good practice to make the constructor throw an exception? [duplicate]

Is it a good practice to make the constructor throw an exception? For example I have a class Person and I have age as its only attribute. Now I provide the class as

8 Answers 8

Throwing exceptions in a constructor is not bad practice. In fact, it is the only reasonable way for a constructor to indicate that there is a problem; e.g. that the parameters are invalid.

I also think that throwing checked exceptions can be OK 1 , assuming that the checked exception is 1) declared, 2) specific to the problem you are reporting, and 3) it is reasonable to expect the caller to deal with a checked exception for this 2 .

However explicitly declaring or throwing java.lang.Exception is almost always bad practice.

You should pick an exception class that matches the exceptional condition that has occurred. If you throw Exception it is difficult for the caller to separate this exception from any number of other possible declared and undeclared exceptions. This makes error recovery difficult, and if the caller chooses to propagate the Exception, the problem just spreads.

1 — Some people may disagree, but IMO there is no difference between this case and the case of throwing exceptions. The standard checked vs unchecked advice applies equally to both cases.

2 — For example, the existing FileInputStream constructors will throw FileNotFoundException if you try to open a file that does not exist. Assuming that it is reasonable for FileNotFoundException to be a checked exception 3 , then the constructor is the most appropriate place for that exception to be thrown. If we threw the FileNotFoundException the first time that (say) a read or write call was made, that is liable to make application logic more complicated.

3 — Given that this is one of the motivating examples for checked exceptions, if you don’t accept this you are basically saying that all exceptions should be unchecked. That is not practical . if you are going to use Java.

Someone suggested using assert for checking arguments. The problem with this is that checking of assert assertions can be turned on and off via a JVM command-line setting. Using assertions to check internal invariants is OK, but using them to implement argument checking that is specified in your javadoc is not a good idea . because it means your method will only strictly implement the specification when assertion checking is enabled.

The second problem with assert is that if an assertion fails, then AssertionError will be thrown, and received wisdom is that it is a bad idea to attempt to catch Error and any of its subtypes.

I’ve always considered throwing checked exceptions in the constructor to be bad practice, or at least something that should be avoided.

The reason for this is that you cannot do this :

Instead you must do this :

At the point when I’m constructing SomeObject I know what it’s parameters are so why should I be expected to wrap it in a try catch? Ahh you say but if I’m constructing an object from dynamic parameters I don’t know if they’re valid or not. Well, you could. validate the parameters before passing them to the constructor. That would be good practice. And if all you’re concerned about is whether the parameters are valid then you can use IllegalArgumentException.

So instead of throwing checked exceptions just do

Of course there are cases where it might just be reasonable to throw a checked exception

But how often is that likely?

As mentioned in another answer here, in Guideline 7-3 of the Java Secure Coding Guidelines, throwing an exception in the constructor of a non-final class opens a potential attack vector:

Guideline 7-3 / OBJECT-3: Defend against partially initialized instances of non-final classes When a constructor in a non-final class throws an exception, attackers can attempt to gain access to partially initialized instances of that class. Ensure that a non-final class remains totally unusable until its constructor completes successfully.

From JDK 6 on, construction of a subclassable class can be prevented by throwing an exception before the Object constructor completes. To do this, perform the checks in an expression that is evaluated in a call to this() or super().

For compatibility with older releases, a potential solution involves the use of an initialized flag. Set the flag as the last operation in a constructor before returning successfully. All methods providing a gateway to sensitive operations must first consult the flag before proceeding:

Furthermore, any security-sensitive uses of such classes should check the state of the initialization flag. In the case of ClassLoader construction, it should check that its parent class loader is initialized.

Partially initialized instances of a non-final class can be accessed via a finalizer attack. The attacker overrides the protected finalize method in a subclass and attempts to create a new instance of that subclass. This attempt fails (in the above example, the SecurityManager check in ClassLoader’s constructor throws a security exception), but the attacker simply ignores any exception and waits for the virtual machine to perform finalization on the partially initialized object. When that occurs the malicious finalize method implementation is invoked, giving the attacker access to this, a reference to the object being finalized. Although the object is only partially initialized, the attacker can still invoke methods on it, thereby circumventing the SecurityManager check. While the initialized flag does not prevent access to the partially initialized object, it does prevent methods on that object from doing anything useful for the attacker.

Читать еще:  В приложении хром произошла ошибка

Use of an initialized flag, while secure, can be cumbersome. Simply ensuring that all fields in a public non-final class contain a safe value (such as null) until object initialization completes successfully can represent a reasonable alternative in classes that are not security-sensitive.

A more robust, but also more verbose, approach is to use a «pointer to implementation» (or «pimpl»). The core of the class is moved into a non-public class with the interface class forwarding method calls. Any attempts to use the class before it is fully initialized will result in a NullPointerException. This approach is also good for dealing with clone and deserialization attacks.

Инстанцируем java.lang.Class

Конструктор java.lang.Class является одной из самых охраняемых сущностей в языке Java. В спецификации чётко сказано, что объекты типа Class может создавать только сама JVM и что нам тут делать нечего, но так ли это на самом деле?

Предлагаю погрузиться в глубины Reflection API (и не только) и выяснить, как там всё устроено и насколько трудно будет обойти имеющиеся ограничения.

Эксперимент я провожу на 64-битной JDK 1.8.0_151 с дефолтными настройками. Про Java 9 будет в самом конце статьи.

Уровень 1. Простой

Начнём с самых наивных попыток и пойдём по нарастающей. Сперва посмотрим врагу в лицо:

Ничего особенного этот конструктор собой не представляет. Компилятор не делает для него никаких исключений, и в байткоде конструктор тоже присутствует. Поэтому попробуем поступить так же, как мы бы поступили с любым другим классом:

Вполне ожидаемо данный код не будет работать и выдаст следующую ошибку:

С первой же попытки мы попали на первое предупреждение из метода setAccessible0 . Оно захардкожено специально для конструктора класса java.lang.Class :

Не проблема, ведь ключевой строкой в этом методе является последняя — установка поля override в значение true . Это легко сделать, используя грубую силу:

Уровень 2. Посложнее

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

Нас занесло прямиком в класс пакета sun.reflect , а мы знаем, что основная магия должна происходить именно там. Самое время заглянуть в реализацию newInstance класса Constructor и узнать, как мы туда попали:

Из реализации становится понятно, что Constructor делегирует всю работу по инстанцированию другому объекту типа ConstructorAccessor . Он инициализируется ленивым образом и в дальнейшем не меняется. Внутренности метода acquireConstructorAccessor описывать не стану, скажу лишь, что в результате он приводит к вызову метода newConstructorAccessor объекта класса sun.reflect.ReflectionFactory . И именно для конструктора класса java.lang.Class (а ещё для абстрактных классов) данный метод возвращает объект InstantiationExceptionConstructorAccessorImpl . Он не умеет ничего инстанцировать, а только бросается исключениями на каждом обращении к нему. Всё это означает лишь одно: правильный ConstructorAccessor придётся инстанцировать самим.

Уровень 3. Нативный

Время узнать, каких вообще типов бывают объекты ConstructorAccessor (помимо описанного выше):

  • BootstrapConstructorAccessorImpl :
    используется для инстанцирования классов, которые сами являются реализацией ConstructorAccessor . Вероятно, спасает какой-то код от бесконечной рекурсии. Штука узкоспециализированная, трогать я её не буду;
  • GeneratedConstructorAccessor :
    самая интересная реализация, о которой я расскажу подробно, но позже;
  • связка NativeConstructorAccessorImpl и DelegatingConstructorAccessorImpl :
    то, что возвращается по умолчанию, и поэтому рассмотрится мною в первую очередь. DelegatingConstructorAccessorImpl попросту делегирует свою работу другому объекту, хранящемуся у него в поле. Плюс данного подхода в том, что он позволяет подменить реализацию на лету. Именно это на самом деле и происходит — NativeConstructorAccessorImpl для каждого конструктора отрабатывает максимум столько раз, сколько указано в системном свойстве sun.reflect.inflationThreshold (15 по умолчанию), после чего подменяется на GeneratedConstructorAccessor . Справедливости ради стоит добавить, что установка свойства sun.reflect.noInflation в значение «true» по сути сбрасывает inflationThreshhold в ноль, и NativeConstructorAccessorImpl перестаёт создаваться в принципе. По умолчанию это свойство имеет значение «false» .

Итак, для самого обычного класса при самых обычных обстоятельствах мы бы получили объект
NativeConstructorAccessorImpl , а значит, именно его и попробуем создать вручную:

Здесь нет никаких подвохов: объект создаётся без лишних ограничений, и всё, что нам остаётся, так это с его помощью инстанцировать java.lang.Class :

Но тут ждёт сюрприз:

Кажется, JVM не ожидает от пользователя столь нелогичных действий, особенно после всех предупреждений. Тем не менее, данный результат можно по праву считать достижением — завалил JVM, ни разу не воспользовавшись классами пакета sun.misc !

Уровень 4. Магический

Нативный вызов не работает — значит, теперь нужно разобраться с GeneratedConstructorAccessor .

На самом деле, это не просто класс, а целое семейство классов. Для каждого конструктора в рантайме генерируется своя уникальная реализация. Именно поэтому в первую очередь используется нативная реализация: генерировать байткод и создавать из него класс дело затратное. Сам процесс генерации класса запрятан в метод generateConstructor класса sun.reflect.MethodAccessorGenerator . Вызвать его вручную не составит труда:

Читать еще:  Arraylist java примеры

Как и в случае с NativeConstructorAccessorImpl , тут нет подводных камней — данный код отработает и сделает ровно то, что от него ждут. Но давайте задумаемся на минутку: ну сгенерировали мы какой-то класс, откуда у него возьмутся права на вызов приватного конструктора? Такого быть не должно, поэтому мы просто обязаны сдампить сгенерированный класс и изучить его код. Сделать это несложно — встаём отладчиком в метод generateConstructor и в нужный момент дампим нужный нам массив байт в файл. Декомпилированная его версия выглядит следующим образом (после переименования переменных):

Такой код, естественно, обратно не скомпилируется, и этому есть две причины:

  • вызов new Class без скобочек. Он соответствует инструкции NEW , которая выделяет память под объект, но конструктор у него не вызывает;
  • вызов clazz. (classLoader) — это как раз вызов конструктора, который в таком явном виде в языке Java невозможен.

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

Если закрыть глаза на нетипичную обработку исключений, то во всём остальном данный класс абсолютно нормален, но всё ещё непонятно, откуда у него вдруг права на вызов приватных конструкторов. Оказывается, всё дело в суперклассе:

В JVM есть известный костыль под названием sun.reflect.MagicAccessorImpl . Всякий его наследник обладает неограниченным доступом к любым приватным данным любых классов. Это именно то, что нужно! Раз класс магический, то он поможет получить инстанс java.lang.Class . Проверяем:

и опять получаем исключение:

Вот это уже действительно интересно. Судя по всему, обещанной магии не произошло. Или я ошибаюсь?

Стоит рассмотреть ошибку внимательнее и сравнить её с тем, как должен себя вести метод newInstance . Будь проблема в строке clazz. (classLoader) , мы бы получили InvocationTargetException . На деле же имеем IllegalAccessError , то есть до вызова конструктора дело не дошло. С ошибкой отработала инструкция NEW , не позволив выделить память под объект java.lang.Class . Здесь наши полномочия всё, окончены.

Уровень 5. Современный

Reflection не помог решить проблему. Может быть, дело в том, что Reflection старый и слабый, и вместо него стоит использовать молодой и сильный MethodHandles? Думаю, да. Как минимум, стоит попробовать.

И как только я решил, что Reflection не нужен, он тут же пригодился. MethodHandles — это, конечно, хорошо, но с помощью него принято получать лишь те данные, к которым есть доступ. А если понадобился приватный конструктор, то придётся выкручиваться по старинке.

Итак, нам нужен MethodHandles.Lookup с приватным доступом к классу java.lang.Class . На этот случай есть очень подходящий конструктор:

Получив lookup , можно получить объект MethodHandle , соответствующий требуемому нам конструктору:

После запуска этого метода я был откровенно удивлён — lookup делает вид, что конструктора вообще не существует, хотя он точно присутствует в классе!

Странно то, что причина исключения — NoSuchFieldError . Загадочно.

В этот раз ошибся именно я, но далеко не сразу это понял. Спецификация findConstructor требует, чтобы тип возвращаемого значения был void , несмотря на то, что у результата MethodType будет ровно таким, как я описал (всё потому, что метод , отвечающий за конструктор, действительно возвращает void по историческим причинам).
Так или иначе, путаницы можно избежать, ведь у lookup есть второй метод для получения конструктора, и он называется unreflectConstructor :

Данный метод уж точно корректно отработает и вернёт тот handle, который должен.

Момент истины. Запускаем метод инстанцирования:

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

По умолчанию stacktrace отображается укороченным, поэтому я добавил
-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames в параметры запуска. Так становится проще понять, в какое странное место мы попали.

Не буду углубляться в то, какие классы генерирует MethodHandles , да это и не принципиально. Важно совсем другое — мы наконец-то докопались до использования sun.misc.Unsafe , и даже он не в силах создать объект java.lang.Class .

Метод allocaeInstance используется в тех местах, где нужно создать объект, но не вызывать у него конструктор. Такое бывает полезно, например, при десериализации объектов. По сути, это та же инструкция NEW , но не обременённая проверками прав доступа. Почти не обременённая, как мы только что увидели.

Раз даже Unsafe не смог, мне остаётся лишь прийти к печальному заключению: аллоцировать новый объект java.lang.Class невозможно. Интересно выходит — думал, что запрещён конструктор, а запрещена аллокация! Попробуем это дело обойти.

Уровень 6. Небезопасный

Предлагаю создать пустой объект и взглянуть, из чего же он состоит. Для этого возьмём Unsafe и аллоцируем новенький java.lang.Object :

На текущей JVM результатом будет область памяти в 12 байт, выглядящая вот так:

То, что вы здесь видите, это «заголовок объекта». По большому счёту, он состоит из двух частей — 8 байт markword, которые нас не интересуют, и 4 байта classword, которые важны.

Каким образом JVM узнаёт класс объекта? Она делает это путём чтения области classword, которая хранит указатель на внутреннюю структуру JVM, описывающую класс. Значит если в данное место записать другое значение, то и класс объекта изменится!

Читать еще:  Java string getbytes

Дальнейший код очень, очень плохой, никогда так не делайте:

Мы прочитали classword объекта Object.class и записали его в classword объекта object . Результат работы следующий:

С натяжкой можно считать, что java.lang.Class мы аллоцировали. Мы молодцы! Теперь надо вызвать конструктор. Вы можете смеяться, но сейчас мы будем с помощью ASM генерировать класс, умеющий вызывать нужный конструктор. Естественно, при этом нужно унаследоваться от MagicAccessorImpl .

Так начинается создание класса (константы импортированы статически, так короче):

Так ему создаётся конструктор:

А так создаётся метод void construct(Class , ClassLoader) , который внутри себя вызывает конструктор у объекта Class :

Класс готов. Осталось загрузить, инстанцировать и вызвать нужный метод:

И это работает! Точнее так: повезло, что работает. Можно проверить, запустив следующий код:

Вывод будет таким:

О том, в какую область памяти записался этот ClassLoader и откуда потом прочитался, я тактично умолчу. И, как ожидалось, вызов практически любого другого метода на данном объекте приводит к немедленному краху JVM. А в остальном — цель выполнена!

Что там в Java 9?

В Java 9 всё почти так же. Можно проделать все те же действия, но с несколькими оговорками:

  • в параметры компилятора надо добавить —add-exports java.base/jdk.internal.reflect=sample (где sample — это имя вашего модуля);
  • в параметры запуска надо добавить:
    —add-opens java.base/jdk.internal.reflect=sample
    —add-opens java.base/java.lang=sample
    —add-opens java.base/java.lang.reflect=sample
    —add-opens java.base/java.lang.invoke=sample
    —add-opens java.base/jdk.internal.reflect=java.base
  • в зависимости модуля надо добавить requires jdk.unsupported ;
  • у конструктора java.lang.Class поменялась сигнатура, надо учесть.

Так же стоит учесть, что sun.reflect перенесли в jdk.internal.reflect и что класс MyConstructorInvocator теперь надо грузить тем же загрузчиком, что у MagicAccessorImpl .
ClassLoader.getSystemClassLoader() уже не сработает, у него не будет доступа.

Ещё исправили странную багу с NoSuchFieldError : теперь на его месте NoSuchMethodError , который там и должен быть. Мелочь, но приятно.

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

Могут ли конструкторы создавать исключения в Java?

конструкторам разрешено создавать исключения?

8 ответов

да, конструкторы могут создавать исключения. Обычно это означает, что новый объект сразу же имеет право на сборку мусора (хотя он может не собираться в течение некоторого времени, конечно). Однако «наполовину построенный» объект может остаться, если он стал видимым ранее в конструкторе (например, назначив статическое поле или добавив себя в коллекцию).

одна вещь, чтобы быть осторожным о генерации исключений в конструкторе: потому что вызывающий (обычно) не будет иметь возможности использовать новый объект, конструктор должен быть осторожным, чтобы избежать получения неуправляемых ресурсов (дескрипторы файлов и т. д.), а затем бросать исключение, не освобождая их. Например, если конструктор пытается открыть FileInputStream и FileOutputStream , и первый преуспевает, но второй терпит неудачу, вы должны попытаться закрыть первый поток. Это становится сложнее, если это конструктор подкласса, который выдает исключение, конечно. все становится немного сложнее. Это не проблема очень часто, но ее стоит рассмотреть.

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

частично инициализированные экземпляры не-конечного класса могут быть доступны через атаку финализатора. Злоумышленник переопределяет метод finalize защищены в подклассе, и пытается создать новый экземпляр этого подкласса. Эта попытка терпит неудачу (в приведенном выше например, проверка SecurityManager в конструкторе загрузчика классов вызывает исключение безопасности), но злоумышленник просто игнорирует любое исключение и ждет, пока виртуальная машина выполнит завершение для частично инициализированного объекта. Когда это происходит, вызывается вредоносная реализация метода finalize, предоставляющая злоумышленнику доступ к этому, ссылка на завершаемый объект. Хотя объект инициализирован только частично, злоумышленник все равно может вызывать методы на нем (таким образом обход проверки SecurityManager).

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

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

да, конструкторам разрешено создавать исключения.

однако, будьте очень мудры в выборе того, какие исключения они должны быть-проверенные исключения или непроверенные. Непроверенные исключения в основном являются подклассами RuntimeException.

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

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

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

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

да, он может вызвать исключение, как и другой метод

Ссылка на основную публикацию
Adblock
detector
×
×