Наконец-то команда разработки компании 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, она избавляет нас от излишней вложенности блоков кода одних в другие, если мы не разбиваем операции на методы. Ей не нужно увлекаться, но если необходимо, то почему нет?
Комментарии (15)
Bonart
01.10.2017 02:41-1Код очень конкретный в силу перегруженности прямым управлением ресурсами.
Его логично разбить на методы (или классы) для получения, преобразование и записи данных, каждый из которых использует только те соединения, запросы и парсеры, какие нужны ему самому.
Конкретные фабрики ресурсов передавать из точки сборки.
Короче, использовать DI и будет вам счастье.
И да, возможности Java 8 смотрятся намного лучше магических аннотаций, совершенно непонятно, что там такого ужасного для автора.vtarasoff Автор
01.10.2017 20:08+1Ужасного там то, что он работает только на Java 8, а также то, что появляется какая-то магическая переменная fake, которая используется только для того, чтобы выполнить код очистки ресурсов, и вот это как раз-таки на первый взгляд не очевидно
Bonart
01.10.2017 20:25Ужасного там то, что он работает только на Java 8
Если у проекта нет требования совместимости с ранними версиями, то какая разница?
появляется какая-то магическая переменная fake
Это не магия, а полноценная часть языка. И ответственность данной переменной простая и понятная — дать возможность очистки для объекта, которые не умеет делать это самостоятельно. Хотя, конечно, приятнее было бы оставить просто выражение.
А вот специальные атрибуты, повязанные на конкретную библиотеку — самая что ни на есть магия со всеми вытекающими.
vtarasoff Автор
01.10.2017 20:41В данном случае единственным бескостыльным методом является:
try { ... открывает ресурс ... } finally { ... правильно закрываем ресурс, если он не null ... }
Все остальные варианты — это костыли (за исключением обертки, но ее писать не удобно). И @Cleanup — это аннотация, которая помогает сделать более менее красивый костыль. Переменная fake и использование передачи метода, чтобы только закрыть ресурс — она просто ужасна:
Resource res; // вот тут она объявляется только потому, что внутри try ее не объявить - это жесть №1 try (Autoclose fake = (res = ResourceCreator.create())::free) { // все, fakе больше нигде не используется, это жесть №2 ... тут уже полезный код ... }
Я же не говорю, что давайте использовать @Cleanup везде, нет. Если метод маленький и там один такой ресурс с free, то не беда, давайте просто try-finally сделаем. Но когда идет работа с потоками, один вкладывается в другой, разные врайтеры, которые не закрывают ресурсы, когда закрываются сами, и если их много, то почему нет? Имхо одна строчка
@Cleanup("free") Resource = ...
куда понятнее и более читаемая.Bonart
02.10.2017 00:24В данном случае единственным бескостыльным методом является
Вы считаете бескостыльным только явное?
Здесь с вами многие не согласятся.
Лично я полагаю, что шаблонный код должен быть как можно меньше. Это позволяет при чтении сосредоточиться на реализованном алгоритме вместо шума.
Переменная fake и использование передачи метода, чтобы только закрыть ресурс — она просто ужасна
Переменная, когда достаточно выражения, действительно смотрится не очень красиво. Но таких мест в яве много, для тех кому нестерпимо — есть скала, котлин и прочие языки для JVM.
Полагаю, что эффект можно смягчить с помощью обобщенной обертки, параметризованной типом содержимого и методом очистки. Тогда переменная останется только одна.
Что до использования передачи метода — это заведомо лучше атрибута со строковым параметром.
vtarasoff Автор
02.10.2017 01:34- Я тоже за использование шаблонного кода как можно меньше.
- Ничего плохого в строкового имени метода для этапа компиляции нет.
- Приведите код шаблонной обертки.
Bonart
02.10.2017 03:01Ничего плохого в строкового имени метода для этапа компиляции нет.
Тут я с вами не согласен. Я очень уважаю инструменты для вплетения кода, но предпочитаю без крайней нужды не использовать имена программных объектов как строковые константы.
Приведите код шаблонной обертки
import java.lang.*; import java.util.function.Function; public final class Usable<T> implements AutoCloseable { public Usable(T object, Function<T, AutoCloseable> toCloseable) { _object = object; _closeable = toCloseable.apply(object); } public static <T> Usable from(T object, Function<T, AutoCloseable> toCloseable) { return new Usable<T>(object, toCloseable); } public T getObject() { return _object; } public void close() throws Exception { _closeable.close(); } private AutoCloseable _closeable; private T _object; }
Прошу извинить если коряво — я на яве не пишу совсем, по ссылке онлайн-демка.
musicriffstudio
02.10.2017 11:56При сохранении в БД документ сериализуется в XML
дальше можно не читать
Зачем им бд, пусть хранят хмл«ы в отдельной папочке.
szubtsovskiy
Я бы тоже сопротивлялся. Главная проблема с такой аннотацией — это иллюзия читабельности. Если я каждый день с этим не сталкиваюсь (а Юнг и его товарищ, видимо, не сталкиваются), то мне не будет понятно, что происходит, особенно в случае с @Cleanup(«free»). Здравствуй, рефлексия! Отдельный луч ненависти авторам за это. Та самая ситуация, когда абстракция протекла и вместо отказа от неё начали городить костыли.
В общем, удобство это очень субъективное и условное. Особенно, если речь идёт об изменениях в compile time. Действительно, как дебажить потом? Я бы пошёл по пути версии 2 + обёртки.
SirEdvin
Мне всегда казалось, что Lombok срабатывает во время сборки кода. Я не прав?
vtarasoff Автор
Дебажить — поставить два бряка, один на код, второй на метод free. Зачем дебажить то, что происходит внутри finally? Я все-таки надеюсь, что люди, пишущие Lombok умные, и сделали там все правильно.
anton_t
Никакой рефлексии. Lombok во время компиляции преобразует
в try-finally с вызовом метода free. А если такого метода нет, то возникнет ошибка компиляции. Да и плагин Lombok-а к IDEA (или к Eclipse) во время написания кода подскажет, что такого метода нет.Lombok это не рефлексия, а расширение языка через annotation processors.
dimkrayan
Насколько я помню (правда, пользовался только для других методов), в Idea есть кнопка «Delombok», которая превращает аннотации в скрывающийся за ними код. Вот ее и использовать при дебаге.
vtarasoff Автор
Да тут не надо использовать delombok для дебага) Зачем дебажить то, что в finally-блоке? Сам код надо дебажить и free.