Это вымышленная история, и все совпадения случайны.

Наконец-то команда разработки компании 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)


  1. szubtsovskiy
    30.09.2017 20:26
    +5

    Я бы тоже сопротивлялся. Главная проблема с такой аннотацией — это иллюзия читабельности. Если я каждый день с этим не сталкиваюсь (а Юнг и его товарищ, видимо, не сталкиваются), то мне не будет понятно, что происходит, особенно в случае с @Cleanup(«free»). Здравствуй, рефлексия! Отдельный луч ненависти авторам за это. Та самая ситуация, когда абстракция протекла и вместо отказа от неё начали городить костыли.

    В общем, удобство это очень субъективное и условное. Особенно, если речь идёт об изменениях в compile time. Действительно, как дебажить потом? Я бы пошёл по пути версии 2 + обёртки.


    1. SirEdvin
      30.09.2017 20:40
      +2

      Мне всегда казалось, что Lombok срабатывает во время сборки кода. Я не прав?


    1. vtarasoff Автор
      01.10.2017 20:09

      Дебажить — поставить два бряка, один на код, второй на метод free. Зачем дебажить то, что происходит внутри finally? Я все-таки надеюсь, что люди, пишущие Lombok умные, и сделали там все правильно.


    1. anton_t
      01.10.2017 20:11
      +1

      Никакой рефлексии. Lombok во время компиляции преобразует

      @Cleanup("free")
      в try-finally с вызовом метода free. А если такого метода нет, то возникнет ошибка компиляции. Да и плагин Lombok-а к IDEA (или к Eclipse) во время написания кода подскажет, что такого метода нет.
      Lombok это не рефлексия, а расширение языка через annotation processors.


    1. dimkrayan
      01.10.2017 20:11

      Насколько я помню (правда, пользовался только для других методов), в Idea есть кнопка «Delombok», которая превращает аннотации в скрывающийся за ними код. Вот ее и использовать при дебаге.


      1. vtarasoff Автор
        01.10.2017 20:12
        +1

        Да тут не надо использовать delombok для дебага) Зачем дебажить то, что в finally-блоке? Сам код надо дебажить и free.


  1. Bonart
    01.10.2017 02:41
    -1

    Код очень конкретный в силу перегруженности прямым управлением ресурсами.
    Его логично разбить на методы (или классы) для получения, преобразование и записи данных, каждый из которых использует только те соединения, запросы и парсеры, какие нужны ему самому.
    Конкретные фабрики ресурсов передавать из точки сборки.
    Короче, использовать DI и будет вам счастье.
    И да, возможности Java 8 смотрятся намного лучше магических аннотаций, совершенно непонятно, что там такого ужасного для автора.


    1. vtarasoff Автор
      01.10.2017 20:08
      +1

      Ужасного там то, что он работает только на Java 8, а также то, что появляется какая-то магическая переменная fake, которая используется только для того, чтобы выполнить код очистки ресурсов, и вот это как раз-таки на первый взгляд не очевидно


      1. Bonart
        01.10.2017 20:25

        Ужасного там то, что он работает только на Java 8

        Если у проекта нет требования совместимости с ранними версиями, то какая разница?


        появляется какая-то магическая переменная fake

        Это не магия, а полноценная часть языка. И ответственность данной переменной простая и понятная — дать возможность очистки для объекта, которые не умеет делать это самостоятельно. Хотя, конечно, приятнее было бы оставить просто выражение.


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


        1. 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 = ...
          

          куда понятнее и более читаемая.


          1. Bonart
            02.10.2017 00:24

            В данном случае единственным бескостыльным методом является

            Вы считаете бескостыльным только явное?
            Здесь с вами многие не согласятся.
            Лично я полагаю, что шаблонный код должен быть как можно меньше. Это позволяет при чтении сосредоточиться на реализованном алгоритме вместо шума.


            Переменная fake и использование передачи метода, чтобы только закрыть ресурс — она просто ужасна

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


            Что до использования передачи метода — это заведомо лучше атрибута со строковым параметром.


            1. vtarasoff Автор
              02.10.2017 01:34

              1. Я тоже за использование шаблонного кода как можно меньше.
              2. Ничего плохого в строкового имени метода для этапа компиляции нет.
              3. Приведите код шаблонной обертки.


              1. 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;
                }

                Прошу извинить если коряво — я на яве не пишу совсем, по ссылке онлайн-демка.


  1. AKiNO
    01.10.2017 20:15
    -1

    сейчас все поделилось на тех, кто пишет на Java и тех, кто пишет на Lombok


  1. musicriffstudio
    02.10.2017 11:56

    При сохранении в БД документ сериализуется в XML


    дальше можно не читать

    Зачем им бд, пусть хранят хмл«ы в отдельной папочке.