Одновременная запись в файл XML
У меня есть несколько процессов, запущенных на разных компьютерах, которые необходимы для чтения / записи в общий XML-файл, для этого я использую DOM with Java
а также FileLocks
(Хотя я знаю, что база данных будет более эффективным подходом, это нежизнеспособно из-за ограничений проекта) .
Чтобы внести изменения в файл XML, соответствующий процесс сначала создает исключительно заблокированный канал, который используется для чтения файла, затем он пытается повторно использовать этот же канал для записи новой версии перед закрытием канала; таким образом, замок никогда не сломается. Однако проблема в том, что я получаю java.nio.channels.ClosedChannelException
при попытке записать результат, хотя я никогда явно не закрывал канал. У меня есть подозрения, что строка кода:
doc = dBuilder.parse(Channels.newInputStream(channel));
закрывает канал Если так, как я мог заставить канал оставаться открытым? Мой код можно увидеть ниже:
[удален код после обновления]
ОБНОВЛЕНИЕ: Размещение System.out.println(channel.isOpen())
до и после подозрительной строки кода подтверждается, что именно здесь канал закрыт.
ОБНОВЛЕНИЕ: задав отдельный вопрос, код ниже теперь предотвращает закрытие канала во время операции разбора. Теперь проблема в том, что вместо замены исходного XML-файла преобразователь добавляет измененный документ в исходный. В документации я не могу найти какие-либо связанные параметры для указания вывода Transformer.transform
(Я искал Transformer
/Transformer factory
/StreamResult
) . Я что-то пропустил? Нужно ли как-то очистить канал перед записью? Благодарю.
ОБНОВЛЕНИЕ: Наконец-то решена проблема с добавлением, обрезав канал до размера 0. Спасибо @JLRishe за совет. Выложили рабочий код в качестве ответа.
2 ответа
Это код, который, наконец, работает! Смотрите обновления вопросов для объяснения различных частей.
import java.io.*;
import java.nio.channels.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
public class Test2{
String path = "...Test 2.xml";
public Test2(){
Document doc = null;
DocumentBuilderFactory dbFactory;
DocumentBuilder dBuilder;
NodeList itemList;
Transformer transformer;
FileChannel channel;
Element newElement;
int prevNumber;
TransformerFactory transformerFactory ;
DOMSource source;
StreamResult result;
NonClosingInputStream ncis = null;
try {
channel = new RandomAccessFile(new File(path), "rw").getChannel();
FileLock lock = channel.lock(0L, Long.MAX_VALUE, false);
try {
dbFactory = DocumentBuilderFactory.newInstance();
dBuilder = dbFactory.newDocumentBuilder();
ncis = new NonClosingInputStream(Channels.newInputStream(channel));
doc = dBuilder.parse(ncis);
} catch (SAXException | IOException | ParserConfigurationException e) {
e.printStackTrace();
}
doc.getDocumentElement().normalize();
itemList = doc.getElementsByTagName("Item");
newElement = doc.createElement("Item");
prevNumber = Integer.parseInt(((Element) itemList.item(itemList.getLength() - 1)).getAttribute("Number"));
newElement.setAttribute("Number", (prevNumber + 1) + "");
doc.getDocumentElement().appendChild(newElement);
transformerFactory = TransformerFactory.newInstance();
transformer = transformerFactory.newTransformer();
source = new DOMSource(doc);
channel.truncate(0);
result = new StreamResult(Channels.newOutputStream(channel));
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.transform(source, result);
channel.close();
} catch (IOException | TransformerException e) {
e.printStackTrace();
} finally {
try {
ncis.reallyClose();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class NonClosingInputStream extends FilterInputStream {
public NonClosingInputStream(InputStream it) {
super(it);
}
@Override
public void close() throws IOException {
// Do nothing.
}
public void reallyClose() throws IOException {
// Actually close.
in.close();
}
}
public static void main(String[] args){
new Test2();
}
}
Попробуйте этот дизайн вместо:
- Создайте новый сервис (процесс), который открывает сокет и слушает "команды обновления".
- Все остальные процессы не записывают в файл напрямую, а вместо этого отправляют "команды обновления" в новый сервис
Таким образом, вам никогда не нужно беспокоиться о блокировке. Чтобы сделать все это более надежным, вы можете добавить буферы к процессам отправки, чтобы они могли продолжать работать некоторое время, когда служба не работает.
При таком подходе вам никогда не придется иметь дело с блокировками файлов (которые могут быть ненадежными в зависимости от вашей ОС). Сокет также гарантирует, что вы не сможете запустить службу дважды.