Это вымышленная история, и все совпадения случайны.
Наконец-то команда разработки компании Unknown Ltd. выпустила релиз вовремя. Руководитель отдела разработки Эндрю, системный архитектор Юг и простой рядовой разработчик Боб собрались на планирование.
В предстоящий квартал они решили взять больше задач из тех. долга, так как практически весь предыдущий был посвящен исправлению багов и выполнению срочных доработок под конкретных клиентов.
Все уселись поудобнее и начали обсуждать предстоящий план. Боб сразу обратил внимание на задачу по переработке генерации документов. Суть задачи заключалась в том, что генерируемые документы состоят из схемы и настроек генерации и непосредственно самого документа. При сохранении в БД документ сериализуется в XML, конвертируется в поток байт и сжимается, а потом все стандартненько — помещается в колонку типа BLOB. Когда нужно отобразить в системе или выгрузить документ, то все повторяется с точностью да наоборот, и вуаля, документик красуется на экране клиента. Вот так, все просто. Но, как известно, дьявол кроется в мелочах. Чтобы заново сгенерировать документ, если необходимо изменить настройки, то приходится целиком загружать весь документ из БД, хотя содержимое его совершенно не нужно. Ай-я-яй. Обсудили задачу. Пришли к выводу, что Бобу предстоит сделать следующее:
На том и порешили. Перешли к обсуждению остальных вопросов.
Прошел час или полтора.
Боб вернулся с планирования воодушевленный, не успев сесть на рабочее место он перевел задачу в «В работе» и начал выполнение. Прошло около двух дней, и первая часть была закончена. Боб благополучно закоммитил изменения и отправил их на ревью. Чтобы даром не терять время он приступил к выполнению второй части — миграции. Спустя некоторое время родился следующий код мигратора:
Чтобы воспользоваться мигратором, клиентский код должен создать или каким-либо образом заинжектить мигратор и вызвать у него метод migrate(). Вот и все.
Кажется что-то не так, подумал Боб. Ну конечно, он же забыл освободить ресурсы. Представьте себе, что если у клиента на продакшене порядка несколько сотен тысяч документов, и мы не освобождаем ресурсов. Боб быстренько починил проблему:
О ужас! Подумал Боб. Как теперь в этом во всем разобраться? Этот код сложно понять не только другому разработчику, но и мне, если я вернусь к нему, предположим через месяц, чтобы что-то исправить или добавить. Надо декомпозировать, подумал Боб, и разбил независимые части кода на методы:
Все, как и прежде, клиентский код создает мигратор и вызывает migrate(). Внутри загружаются все идентификаторы текущих документов, и для каждого идентификатора загружается содержимое документов, которое с помощью SAX парсера разбирается на части, принадлежащие схеме и данным, чтобы потом их сохранить в новую таблицу, но уже раздельно в разные колонки.
Вроде стало немного лучше. Но Бобу взгрустнулось. Почему XMLStreamWriter и Blob не реализуют AutoСloseable, подумал он и начал написал обертки:
Были написаны две обертки SmartXMLStreamWriter и SmartBlob, которые автоматически закрывали XMLStreamWriter и Blob в try-with-resources.
А если у меня появятся еще ресурсы, которые не реализуют AutoCloseable, то мне снова придется писать обертки? Боб обратился за помощью к Югу. Юг немного покумекав выдал оригинальное решение, используя возможности Java 8:
Да-да-да, именно, что вы и подумали: он воспользовался возможностью передачи метода. Код получился просто ужасный. Но зато не нужно писать обертки, подумал Боб и заплакал.
И тут он обратил внимание на аннотацию, которую так активно уже использовал: @RequiredArgsConstructor. Эврика! В библиотеке Lombok есть аннотация @Cleanup, которая как раз и рождена для того, чтобы утешить потерявшего всякие надежды Java программиста. Она на этапе компиляции добавляет в байт-код try-finally и автоматически добавляет код безопасного закрытия ресурсов. Более того, она умеет работать с любым методом освобождения ресурсов, будь то close(), free() или какой-нибудь другой, главное ей об этом подсказать (хотя она и сама умная и выругается, если не нашла подходящего метода).
И Боб переписал проблемные места с использованием @Cleanup:
Довольный найденным элегантным, и главное, из коробки, решением Боб сделал долгожданный коммит и отдал код на ревью.
Ничто не предвещало беды. Но неприятности всегда подстерегают нас за углом. Коммит не прошел ревью, Юг и Эндрю отнюдь не одобрили @Cleanup. Всего два места, где используются не AutoCloseable ресурсы, говорили они. Какой профит это нам даст? Нам не нравится эта аннотация! Как мы будем дебажить код в случае чего? И все в таком духе. Боб безжалостно отбивался, но все попытки были тщетны. И тогда он предпринял еще попытку доказать удобство и выкатил следующий код:
Да-да. Он убрал все дополнительные методы и снова вернул последовательный процедурный код. Работоспособность, конечно, он не успел проверить, потому что так хотелось показать простоту. В этом коде, наверное, уже разберется не только он сам через месяц, а любой другой, кому придется его читать.
Но он снова не нашел поддержки. Как ни бился он о стену — стена оказалась прочней. И он сдался. В продакшен в итоге выкатили код MigratorV5 — тот самый, где так неуклюже используются возможности Java 8.
Эпилог.
Конечно, код который был приведен, далек от идеала, и его еще причесывать, какие-то вещи можно переписать совсем по-другому, например, с помощью шаблонного кода. Последний вариант является вообще воплощением процедурного стиля программирования, что не очень хорошо (но при этом понятно при чтении сверху-вниз). Но суть не в этом. @Cleanup — классная аннотация, которая помогает именно в таких моментах, когда мы не можем воспользоваться try-with-resources, она избавляет нас от излишней вложенности блоков кода одних в другие, если мы не разбиваем операции на методы. Ей не нужно увлекаться, но если необходимо, то почему нет?
Наконец-то команда разработки компании 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<Long> loadIds() throws Exception { List<Long> 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<Long> 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<Long> loadIds() throws Exception { List<Long> 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<Long> 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<Long> loadIds() throws Exception { List<Long> 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<Long> 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<Long> loadIds() throws Exception { List<Long> 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<Long> 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, она избавляет нас от излишней вложенности блоков кода одних в другие, если мы не разбиваем операции на методы. Ей не нужно увлекаться, но если необходимо, то почему нет?
