[Из песочницы] Работа с ресурсами, или как я пропихивал @Cleanup
Это вымышленная история, и все совпадения случайны.
Наконец-то команда разработки компании Unknown Ltd. выпустила релиз вовремя. Руководитель отдела разработки Эндрю, системный архитектор Юг и простой рядовой разработчик Боб собрались на планирование.
В предстоящий квартал они решили взять больше задач из тех. долга, так как практически весь предыдущий был посвящен исправлению багов и выполнению срочных доработок под конкретных клиентов.
Все уселись поудобнее и начали обсуждать предстоящий план. Боб сразу обратил внимание на задачу по переработке генерации документов. Суть задачи заключалась в том, что генерируемые документы состоят из схемы и настроек генерации и непосредственно самого документа. При сохранении в БД документ сериализуется в XML, конвертируется в поток байт и сжимается, а потом все стандартненько — помещается в колонку типа BLOB. Когда нужно отобразить в системе или выгрузить документ, то все повторяется с точностью да наоборот, и вуаля, документик красуется на экране клиента. Вот так, все просто. Но, как известно, дьявол кроется в мелочах. Чтобы заново сгенерировать документ, если необходимо изменить настройки, то приходится целиком загружать весь документ из БД, хотя содержимое его совершенно не нужно. Ай-я-яй. Обсудили задачу. Пришли к выводу, что Бобу предстоит сделать следующее:
- удалить ненужные сущности, изменить существующие, создать новые, чтобы разделить схему с настройками и сами данные документа
- написать миграцию, которая создаст новую таблицу для хранения схемы и данных, в которой будут две колонки, одна типа CLOB для хранения XML схемы, и другая типа BLOB для хранения по-прежнему сжатого XML с данными
На том и порешили. Перешли к обсуждению остальных вопросов.
Прошел час или полтора.
Боб вернулся с планирования воодушевленный, не успев сесть на рабочее место он перевел задачу в «В работе» и начал выполнение. Прошло около двух дней, и первая часть была закончена. Боб благополучно закоммитил изменения и отправил их на ревью. Чтобы даром не терять время он приступил к выполнению второй части — миграции. Спустя некоторое время родился следующий код мигратора:
public class MigratorV1 {
private Connection conn; // Injected
private SAXParser xmlParser; // Injected
private XMLOutputFactory xmlFactory; // Injected
public void migrate() throws Exception {
PreparedStatement selectOldContent = conn.prepareStatement("select content from old_data where id = ?");
PreparedStatement insertNewContent = conn.prepareStatement("insert into new_data (id, scheme, data) values (?, ?, ?)");
ResultSet oldIdResult = conn.createStatement().executeQuery("select id from old_data");
while (oldIdResult.next()) {
long id = oldIdResult.getLong(1);
selectOldContent.setLong(1, id);
ResultSet oldContentResult = selectOldContent.executeQuery();
oldContentResult.next();
Blob oldContent = oldContentResult.getBlob(1);
Reader oldContentReader = new InputStreamReader(new GZIPInputStream(oldContent.getBinaryStream()));
StringWriter newSchemeWriter = new StringWriter();
XMLStreamWriter newSchemeXMLWriter = xmlFactory.createXMLStreamWriter(newSchemeWriter);
ByteArrayOutputStream newDataOutput = new ByteArrayOutputStream();
GZIPOutputStream newZippedDataOutput = new GZIPOutputStream(newDataOutput);
XMLStreamWriter newDataXMLWriter = xmlFactory.createXMLStreamWriter(newZippedDataOutput, "utf-8");
xmlParser.parse(new InputSource(oldContentReader), new DefaultHandler() {
// Usage of schemeXMLWriter and dataXMLWriter to write XML into String and byte[]
});
String newScheme = newSchemeWriter.toString();
byte[] newData = newDataOutput.toByteArray();
StringReader newSchemeReader = new StringReader(newScheme);
ByteArrayInputStream newDataInput = new ByteArrayInputStream(newData);
insertNewContent.setLong(1, id);
insertNewContent.setCharacterStream(2, newSchemeReader, newScheme.length());
insertNewContent.setBlob(3, newDataInput, newData.length);
insertNewContent.executeUpdate();
}
}
}
Чтобы воспользоваться мигратором, клиентский код должен создать или каким-либо образом заинжектить мигратор и вызвать у него метод migrate (). Вот и все.
Кажется что-то не так, подумал Боб. Ну конечно, он же забыл освободить ресурсы. Представьте себе, что если у клиента на продакшене порядка несколько сотен тысяч документов, и мы не освобождаем ресурсов. Боб быстренько починил проблему:
public class MigratorV2 {
private Connection conn; // Injected
private SAXParser xmlParser; // Injected
private XMLOutputFactory xmlFactory; // Injected
public void migrate() throws Exception {
try (
PreparedStatement selectOldContent = conn.prepareStatement("select content from old_data where id = ?");
PreparedStatement insertNewContent = conn.prepareStatement("insert into new_data (id, scheme, data) values (?, ?, ?)");
ResultSet oldIdResult = conn.createStatement().executeQuery("select id from old_data");
){
while (oldIdResult.next()) {
long id = oldIdResult.getLong(1);
selectOldContent.setLong(1, id);
try (ResultSet oldContentResult = selectOldContent.executeQuery()) {
oldContentResult.next();
String newScheme;
byte[] newData;
Blob oldContent = null;
try {
oldContent = oldContentResult.getBlob(1);
try (
Reader oldContentReader = new InputStreamReader(new GZIPInputStream(oldContent.getBinaryStream()));
StringWriter newSchemeWriter = new StringWriter();
ByteArrayOutputStream newDataOutput = new ByteArrayOutputStream();
GZIPOutputStream newZippedDataOutput = new GZIPOutputStream(newDataOutput);
){
XMLStreamWriter newSchemeXMLWriter = null;
XMLStreamWriter newDataXMLWriter = null;
try {
newSchemeXMLWriter = xmlFactory.createXMLStreamWriter(newSchemeWriter);
newDataXMLWriter = xmlFactory.createXMLStreamWriter(newZippedDataOutput, "utf-8");
xmlParser.parse(new InputSource(oldContentReader), new DefaultHandler() {
// Usage of schemeXMLWriter and dataXMLWriter to write XML into String and byte[]
});
} finally {
if (newSchemeXMLWriter != null) {
try {
newSchemeXMLWriter.close();
} catch (XMLStreamException e) {}
}
if (newDataXMLWriter != null) {
try {
newDataXMLWriter.close();
} catch (XMLStreamException e) {}
}
}
newScheme = newSchemeWriter.toString();
newData = newDataOutput.toByteArray();
}
} finally {
if (oldContent != null) {
try {
oldContent.free();
} catch (SQLException e) {}
}
}
try (
StringReader newSchemeReader = new StringReader(newScheme);
ByteArrayInputStream newDataInput = new ByteArrayInputStream(newData);
){
insertNewContent.setLong(1, id);
insertNewContent.setCharacterStream(2, newSchemeReader, newScheme.length());
insertNewContent.setBlob(3, newDataInput, newData.length);
insertNewContent.executeUpdate();
}
}
}
}
}
}
О ужас! Подумал Боб. Как теперь в этом во всем разобраться? Этот код сложно понять не только другому разработчику, но и мне, если я вернусь к нему, предположим через месяц, чтобы что-то исправить или добавить. Надо декомпозировать, подумал Боб, и разбил независимые части кода на методы:
public class MigratorV3 {
private Connection conn; // Injected
private SAXParser xmlParser; // Injected
private XMLOutputFactory xmlFactory; // Injected
@RequiredArgsConstructor
private static class NewData {
final String scheme;
final byte[] data;
}
private List loadIds() throws Exception {
List ids = new ArrayList<>();
try (ResultSet oldIdResult = conn.createStatement().executeQuery("select id from old_data")) {
while (oldIdResult.next()) {
ids.add(oldIdResult.getLong(1));
}
}
return ids;
}
private Blob loadOldContent(PreparedStatement selectOldContent, long id) throws Exception {
selectOldContent.setLong(1, id);
try (ResultSet oldContentResult = selectOldContent.executeQuery()) {
oldContentResult.next();
return oldContentResult.getBlob(1);
}
}
private void oldContentToNewData(Reader oldContentReader, StringWriter newSchemeWriter, GZIPOutputStream newZippedDataOutput) throws Exception {
XMLStreamWriter newSchemeXMLWriter = null;
XMLStreamWriter newDataXMLWriter = null;
try {
newSchemeXMLWriter = xmlFactory.createXMLStreamWriter(newSchemeWriter);
newDataXMLWriter = xmlFactory.createXMLStreamWriter(newZippedDataOutput, "utf-8");
xmlParser.parse(new InputSource(oldContentReader), new DefaultHandler() {
// Usage of schemeXMLWriter and dataXMLWriter to write XML into String and byte[]
});
} finally {
if (newSchemeXMLWriter != null) {
try {
newSchemeXMLWriter.close();
} catch (XMLStreamException e) {}
}
if (newDataXMLWriter != null) {
try {
newDataXMLWriter.close();
} catch (XMLStreamException e) {}
}
}
}
private NewData generateNewDataFromOldContent(PreparedStatement selectOldContent, long id) throws Exception {
Blob oldContent = null;
try {
oldContent = loadOldContent(selectOldContent, id);
try (
Reader oldContentReader = new InputStreamReader(new GZIPInputStream(oldContent.getBinaryStream()));
StringWriter newSchemeWriter = new StringWriter();
ByteArrayOutputStream newDataOutput = new ByteArrayOutputStream();
GZIPOutputStream newZippedDataOutput = new GZIPOutputStream(newDataOutput);
){
oldContentToNewData(oldContentReader, newSchemeWriter, newZippedDataOutput);
return new NewData(newSchemeWriter.toString(), newDataOutput.toByteArray());
}
} finally {
if (oldContent != null) {
try {
oldContent.free();
} catch (SQLException e) {}
}
}
}
private void storeNewData(PreparedStatement insertNewContent, long id, String newScheme, byte[] newData) throws Exception {
try (
StringReader newSchemeReader = new StringReader(newScheme);
ByteArrayInputStream newDataInput = new ByteArrayInputStream(newData);
){
insertNewContent.setLong(1, id);
insertNewContent.setCharacterStream(2, newSchemeReader, newScheme.length());
insertNewContent.setBlob(3, newDataInput, newData.length);
insertNewContent.executeUpdate();
}
}
public void migrate() throws Exception {
List ids = loadIds();
try (
PreparedStatement selectOldContent = conn.prepareStatement("select content from old_data where id = ?");
PreparedStatement insertNewContent = conn.prepareStatement("insert into new_data (id, scheme, data) values (?, ?, ?)");
){
for (Long id : ids) {
NewData newData = generateNewDataFromOldContent(selectOldContent, id);
storeNewData(insertNewContent, id, newData.scheme, newData.data);
}
}
}
}
Все, как и прежде, клиентский код создает мигратор и вызывает migrate (). Внутри загружаются все идентификаторы текущих документов, и для каждого идентификатора загружается содержимое документов, которое с помощью SAX парсера разбирается на части, принадлежащие схеме и данным, чтобы потом их сохранить в новую таблицу, но уже раздельно в разные колонки.
Вроде стало немного лучше. Но Бобу взгрустнулось. Почему XMLStreamWriter и Blob не реализуют AutoСloseable, подумал он и начал написал обертки:
public class MigratorV4 {
private Connection conn; // Injected
private SAXParser xmlParser; // Injected
private XMLOutputFactory xmlFactory; // Injected
@RequiredArgsConstructor
private static class NewData {
final String scheme;
final byte[] data;
}
@RequiredArgsConstructor
private static class SmartXMLStreamWriter implements AutoCloseable {
final XMLStreamWriter writer;
@Override
public void close() throws Exception {
writer.close();
}
}
@RequiredArgsConstructor
private static class SmartBlob implements AutoCloseable {
final Blob blob;
@Override
public void close() throws Exception {
blob.free();
}
}
private List loadIds() throws Exception {
List ids = new ArrayList<>();
try (ResultSet oldIdResult = conn.createStatement().executeQuery("select id from old_data")) {
while (oldIdResult.next()) {
ids.add(oldIdResult.getLong(1));
}
}
return ids;
}
private Blob loadOldContent(PreparedStatement selectOldContent, long id) throws Exception {
selectOldContent.setLong(1, id);
try (ResultSet oldContentResult = selectOldContent.executeQuery()) {
oldContentResult.next();
return oldContentResult.getBlob(1);
}
}
private void oldContentToNewData(Reader oldContentReader, StringWriter newSchemeWriter, GZIPOutputStream newZippedDataOutput) throws Exception {
try (
SmartXMLStreamWriter newSchemeXMLWriter = new SmartXMLStreamWriter(xmlFactory.createXMLStreamWriter(newSchemeWriter));
SmartXMLStreamWriter newDataXMLWriter = new SmartXMLStreamWriter(xmlFactory.createXMLStreamWriter(newZippedDataOutput, "utf-8"));
){
xmlParser.parse(new InputSource(oldContentReader), new DefaultHandler() {
// Usage of schemeXMLWriter and dataXMLWriter to write XML into String and byte[]
});
}
}
private NewData generateNewDataFromOldContent(PreparedStatement selectOldContent, long id) throws Exception {
try (
SmartBlob oldContent = new SmartBlob(loadOldContent(selectOldContent, id));
Reader oldContentReader = new InputStreamReader(new GZIPInputStream(oldContent.blob.getBinaryStream()));
StringWriter newSchemeWriter = new StringWriter();
ByteArrayOutputStream newDataOutput = new ByteArrayOutputStream();
GZIPOutputStream newZippedDataOutput = new GZIPOutputStream(newDataOutput);
){
oldContentToNewData(oldContentReader, newSchemeWriter, newZippedDataOutput);
return new NewData(newSchemeWriter.toString(), newDataOutput.toByteArray());
}
}
private void storeNewData(PreparedStatement insertNewContent, long id, String newScheme, byte[] newData) throws Exception {
try (
StringReader newSchemeReader = new StringReader(newScheme);
ByteArrayInputStream newDataInput = new ByteArrayInputStream(newData);
){
insertNewContent.setLong(1, id);
insertNewContent.setCharacterStream(2, newSchemeReader, newScheme.length());
insertNewContent.setBlob(3, newDataInput, newData.length);
insertNewContent.executeUpdate();
}
}
public void migrate() throws Exception {
List ids = loadIds();
try (
PreparedStatement selectOldContent = conn.prepareStatement("select content from old_data where id = ?");
PreparedStatement insertNewContent = conn.prepareStatement("insert into new_data (id, scheme, data) values (?, ?, ?)");
){
for (Long id : ids) {
NewData newData = generateNewDataFromOldContent(selectOldContent, id);
storeNewData(insertNewContent, id, newData.scheme, newData.data);
}
}
}
}
Были написаны две обертки SmartXMLStreamWriter и SmartBlob, которые автоматически закрывали XMLStreamWriter и Blob в try-with-resources.
А если у меня появятся еще ресурсы, которые не реализуют AutoCloseable, то мне снова придется писать обертки? Боб обратился за помощью к Югу. Юг немного покумекав выдал оригинальное решение, используя возможности Java 8:
public class MigratorV5 {
private Connection conn; // Injected
private SAXParser xmlParser; // Injected
private XMLOutputFactory xmlFactory; // Injected
@RequiredArgsConstructor
private static class NewData {
final String scheme;
final byte[] data;
}
private List loadIds() throws Exception {
List ids = new ArrayList<>();
try (ResultSet oldIdResult = conn.createStatement().executeQuery("select id from old_data")) {
while (oldIdResult.next()) {
ids.add(oldIdResult.getLong(1));
}
}
return ids;
}
private Blob loadOldContent(PreparedStatement selectOldContent, long id) throws Exception {
selectOldContent.setLong(1, id);
try (ResultSet oldContentResult = selectOldContent.executeQuery()) {
oldContentResult.next();
return oldContentResult.getBlob(1);
}
}
private void oldContentToNewData(Reader oldContentReader, StringWriter newSchemeWriter, GZIPOutputStream newZippedDataOutput) throws Exception {
XMLStreamWriter newSchemeXMLWriter;
XMLStreamWriter newDataXMLWriter;
try (
AutoCloseable fake1 = (newSchemeXMLWriter = xmlFactory.createXMLStreamWriter(newSchemeWriter))::close;
AutoCloseable fake2 = (newDataXMLWriter = xmlFactory.createXMLStreamWriter(newZippedDataOutput, "utf-8"))::close;
){
xmlParser.parse(new InputSource(oldContentReader), new DefaultHandler() {
// Usage of schemeXMLWriter and dataXMLWriter to write XML into String and byte[]
});
}
}
private NewData generateNewDataFromOldContent(PreparedStatement selectOldContent, long id) throws Exception {
Blob oldContent;
try (
AutoCloseable fake = (oldContent = loadOldContent(selectOldContent, id))::free;
Reader oldContentReader = new InputStreamReader(new GZIPInputStream(oldContent.getBinaryStream()));
StringWriter newSchemeWriter = new StringWriter();
ByteArrayOutputStream newDataOutput = new ByteArrayOutputStream();
GZIPOutputStream newZippedDataOutput = new GZIPOutputStream(newDataOutput);
){
oldContentToNewData(oldContentReader, newSchemeWriter, newZippedDataOutput);
return new NewData(newSchemeWriter.toString(), newDataOutput.toByteArray());
}
}
private void storeNewData(PreparedStatement insertNewContent, long id, String newScheme, byte[] newData) throws Exception {
try (
StringReader newSchemeReader = new StringReader(newScheme);
ByteArrayInputStream newDataInput = new ByteArrayInputStream(newData);
){
insertNewContent.setLong(1, id);
insertNewContent.setCharacterStream(2, newSchemeReader, newScheme.length());
insertNewContent.setBlob(3, newDataInput, newData.length);
insertNewContent.executeUpdate();
}
}
public void migrate() throws Exception {
List ids = loadIds();
try (
PreparedStatement selectOldContent = conn.prepareStatement("select content from old_data where id = ?");
PreparedStatement insertNewContent = conn.prepareStatement("insert into new_data (id, scheme, data) values (?, ?, ?)");
){
for (Long id : ids) {
NewData newData = generateNewDataFromOldContent(selectOldContent, id);
storeNewData(insertNewContent, id, newData.scheme, newData.data);
}
}
}
}
Да-да-да, именно, что вы и подумали: он воспользовался возможностью передачи метода. Код получился просто ужасный. Но зато не нужно писать обертки, подумал Боб и заплакал.
И тут он обратил внимание на аннотацию, которую так активно уже использовал: @RequiredArgsConstructor. Эврика! В библиотеке Lombok есть аннотация @Cleanup, которая как раз и рождена для того, чтобы утешить потерявшего всякие надежды Java программиста. Она на этапе компиляции добавляет в байт-код try-finally и автоматически добавляет код безопасного закрытия ресурсов. Более того, она умеет работать с любым методом освобождения ресурсов, будь то close (), free () или какой-нибудь другой, главное ей об этом подсказать (хотя она и сама умная и выругается, если не нашла подходящего метода).
И Боб переписал проблемные места с использованием @Cleanup:
public class MigratorV6 {
private Connection conn; // Injected
private SAXParser xmlParser; // Injected
private XMLOutputFactory xmlFactory; // Injected
@RequiredArgsConstructor
private static class NewData {
final String scheme;
final byte[] data;
}
private List loadIds() throws Exception {
List ids = new ArrayList<>();
try (ResultSet oldIdResult = conn.createStatement().executeQuery("select id from old_data")) {
while (oldIdResult.next()) {
ids.add(oldIdResult.getLong(1));
}
}
return ids;
}
private Blob loadOldContent(PreparedStatement selectOldContent, long id) throws Exception {
selectOldContent.setLong(1, id);
try (ResultSet oldContentResult = selectOldContent.executeQuery()) {
oldContentResult.next();
return oldContentResult.getBlob(1);
}
}
private void oldContentToNewData(Reader oldContentReader, StringWriter newSchemeWriter, GZIPOutputStream newZippedDataOutput) throws Exception {
@Cleanup XMLStreamWriter newSchemeXMLWriter = xmlFactory.createXMLStreamWriter(newSchemeWriter);
@Cleanup XMLStreamWriter newDataXMLWriter = xmlFactory.createXMLStreamWriter(newZippedDataOutput, "utf-8");
xmlParser.parse(new InputSource(oldContentReader), new DefaultHandler() {
// Usage of schemeXMLWriter and dataXMLWriter to write XML into String and byte[]
});
}
private NewData generateNewDataFromOldContent(PreparedStatement selectOldContent, long id) throws Exception {
@Cleanup("free") Blob oldContent = loadOldContent(selectOldContent, id);
try (
Reader oldContentReader = new InputStreamReader(new GZIPInputStream(oldContent.getBinaryStream()));
StringWriter newSchemeWriter = new StringWriter();
ByteArrayOutputStream newDataOutput = new ByteArrayOutputStream();
GZIPOutputStream newZippedDataOutput = new GZIPOutputStream(newDataOutput);
){
oldContentToNewData(oldContentReader, newSchemeWriter, newZippedDataOutput);
return new NewData(newSchemeWriter.toString(), newDataOutput.toByteArray());
}
}
private void storeNewData(PreparedStatement insertNewContent, long id, String newScheme, byte[] newData) throws Exception {
try (
StringReader newSchemeReader = new StringReader(newScheme);
ByteArrayInputStream newDataInput = new ByteArrayInputStream(newData);
){
insertNewContent.setLong(1, id);
insertNewContent.setCharacterStream(2, newSchemeReader, newScheme.length());
insertNewContent.setBlob(3, newDataInput, newData.length);
insertNewContent.executeUpdate();
}
}
public void migrate() throws Exception {
List ids = loadIds();
try (
PreparedStatement selectOldContent = conn.prepareStatement("select content from old_data where id = ?");
PreparedStatement insertNewContent = conn.prepareStatement("insert into new_data (id, scheme, data) values (?, ?, ?)");
){
for (Long id : ids) {
NewData newData = generateNewDataFromOldContent(selectOldContent, id);
storeNewData(insertNewContent, id, newData.scheme, newData.data);
}
}
}
}
Довольный найденным элегантным, и главное, из коробки, решением Боб сделал долгожданный коммит и отдал код на ревью.
Ничто не предвещало беды. Но неприятности всегда подстерегают нас за углом. Коммит не прошел ревью, Юг и Эндрю отнюдь не одобрили @Cleanup. Всего два места, где используются не AutoCloseable ресурсы, говорили они. Какой профит это нам даст? Нам не нравится эта аннотация! Как мы будем дебажить код в случае чего? И все в таком духе. Боб безжалостно отбивался, но все попытки были тщетны. И тогда он предпринял еще попытку доказать удобство и выкатил следующий код:
public class MigratorV7 {
private Connection conn; // Injected
private SAXParser xmlParser; // Injected
private XMLOutputFactory xmlFactory; // Injected
public void migrate() throws Exception {
@Cleanup PreparedStatement selectOldContent = conn.prepareStatement("select content from old_data where id = ?");
@Cleanup PreparedStatement insertNewContent = conn.prepareStatement("insert into new_data (id, scheme, data) values (?, ?, ?)");
@Cleanup ResultSet oldIdResult = conn.createStatement().executeQuery("select id from old_data");
while (oldIdResult.next()) {
long id = oldIdResult.getLong(1);
selectOldContent.setLong(1, id);
@Cleanup ResultSet oldContentResult = selectOldContent.executeQuery();
oldContentResult.next();
@Cleanup("free") Blob oldContent = oldContentResult.getBlob(1);
@Cleanup Reader oldContentReader = new InputStreamReader(new GZIPInputStream(oldContent.getBinaryStream()));
@Cleanup StringWriter newSchemeWriter = new StringWriter();
@Cleanup XMLStreamWriter newSchemeXMLWriter = xmlFactory.createXMLStreamWriter(newSchemeWriter);
ByteArrayOutputStream newDataOutput = new ByteArrayOutputStream();
@Cleanup GZIPOutputStream newZippedDataOutput = new GZIPOutputStream(newDataOutput);
@Cleanup XMLStreamWriter newDataXMLWriter = xmlFactory.createXMLStreamWriter(newZippedDataOutput, "utf-8");
xmlParser.parse(new InputSource(oldContentReader), new DefaultHandler() {
// Usage of schemeXMLWriter and dataXMLWriter to write XML into String and byte[]
});
String newScheme = newSchemeWriter.toString();
byte[] newData = newDataOutput.toByteArray();
@Cleanup StringReader newSchemeReader = new StringReader(newScheme);
@Cleanup ByteArrayInputStream newDataInput = new ByteArrayInputStream(newData);
insertNewContent.setLong(1, id);
insertNewContent.setCharacterStream(2, newSchemeReader, newScheme.length());
insertNewContent.setBlob(3, newDataInput, newData.length);
insertNewContent.executeUpdate();
}
}
}
Да-да. Он убрал все дополнительные методы и снова вернул последовательный процедурный код. Работоспособность, конечно, он не успел проверить, потому что так хотелось показать простоту. В этом коде, наверное, уже разберется не только он сам через месяц, а любой другой, кому придется его читать.
Но он снова не нашел поддержки. Как ни бился он о стену — стена оказалась прочней. И он сдался. В продакшен в итоге выкатили код MigratorV5 — тот самый, где так неуклюже используются возможности Java 8.
Эпилог.
Конечно, код который был приведен, далек от идеала, и его еще причесывать, какие-то вещи можно переписать совсем по-другому, например, с помощью шаблонного кода. Последний вариант является вообще воплощением процедурного стиля программирования, что не очень хорошо (но при этом понятно при чтении сверху-вниз). Но суть не в этом. @Cleanup — классная аннотация, которая помогает именно в таких моментах, когда мы не можем воспользоваться try-with-resources, она избавляет нас от излишней вложенности блоков кода одних в другие, если мы не разбиваем операции на методы. Ей не нужно увлекаться, но если необходимо, то почему нет?