Одновременная запись в файл 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();
    }
}

Попробуйте этот дизайн вместо:

  1. Создайте новый сервис (процесс), который открывает сокет и слушает "команды обновления".
  2. Все остальные процессы не записывают в файл напрямую, а вместо этого отправляют "команды обновления" в новый сервис

Таким образом, вам никогда не нужно беспокоиться о блокировке. Чтобы сделать все это более надежным, вы можете добавить буферы к процессам отправки, чтобы они могли продолжать работать некоторое время, когда служба не работает.

При таком подходе вам никогда не придется иметь дело с блокировками файлов (которые могут быть ненадежными в зависимости от вашей ОС). Сокет также гарантирует, что вы не сможете запустить службу дважды.

Другие вопросы по тегам