image

Добрый день уважаемые хабровчане. Это моя первая статья, пожалуйста, сильно не ругайтесь.

Об аудировании в Hibernate написано уже немало. Я хочу рассказать о решении не совсем стандартной задачи — записи в таблицу ревизий ID любого пользователя, назначаемого непосредственно перед операцией записи сущности в базу данных. Стандартное решение, предложенное в официальной документации — использование ID пользователя, сохраненного в сессионном компоненте. Но возможна ситуация, когда ID пользователя необходимо подменить. Пример: пользователь совершает операции через взаимодействие с сервером телефонии посредством DTMF сигналов. В данном случае сессию создавать вообще не нужно. Я долго искал решение в интернете, но так ничего и не нашёл, поэтому предлагаю вашему вниманию свою версию. Возможно кому-то из новичков, вроде меня, она окажется полезной.

Почитав документацию, я понял, что аудирование в Hibernate основано на перехватчиках. Это означает, что поток, совершающий обновление сущности в базе данных, отвечает за аудирование — это уже кое-что.

Попробуем извлечь из этого пользу. Создаем stateless компонент, в котором будет храниться статическая map c парами: ID потока — ID пользователя. Метод start добавляет в map ID пользователя (переданного параметром) и ID текущего потока, затем запускает в новой транзакции метод, выполняющий необходимые действия, дожидается окончания метода (транзакции) и удаляет ID потока и пользователя из map.

@Stateless
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class FakeOwnerTransaction {
    
    @Inject
    private Provider<FakeOwnerTransaction> providerFakeOwnerNewTransaction;
    
    private static ThreadLocal<Long> threadOwnerID = = new ThreadLocal<>()
    
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void newCMT(Runnable action) {
        action.run();
    }
    
    public void start(Long personID, Runnable action) {
       threadOwnerID.set(personID);
        try {
            providerFakeOwnerNewTransaction.get().newCMT(action);
        } finally {
            threadOwnerID.remove();
        }
    }
    
    public static Long getFakeChanger() {
        return threadOwnerID.get();
    }
}

Теперь посмотрим, как будет выглядеть реализация RevisionListener. Если текущий поток ассоциирован с ID пользователя, используем этот ID, иначе берем ID пользователя из сессионного компонента UserManager.

public class Audition implements RevisionListener {
    
    @Override
    public void newRevision(Revinfo revinfo) {
        Long personID = FakeOwnerTransaction.getFakeChanger();
        
        if (personID == null) {
            UserManager userManager = SystemUtils.lookup(JNDI_NAME_PREFIX, UserManager.class);
            personID = userManager.getPersonID();
        }
        revinfo.setPersonID(personID);
    }
}

Ну и напоследок попробуем сделать изменение в базе данных, используя ID указанного пользователя.

FakeOwnerTransaction fakeOwnerTransaction = SystemUtils.lookup(JNDI_NAME_PREFIX, FakeOwnerTransaction.class);
fakeOwnerTransaction.start(getPersonID(), new Runnable() {
            
            @Override
            public void run() {
                Dao dao = SystemUtils.lookup(JNDI_NAME_PREFIX, Dao.class);
                dao.add(new Person(“Smirmov”));
            }
        });


Класс SystemUtils
public class SystemUtils {
    
    public static <T> T lookup(String jndiNamePrefix, Class<T> clazz) {
        String jndiName = jndiNamePrefix;
        jndiName += clazz.getSimpleName() + "!" + clazz.getName();
        try {
            return (T) new InitialContext().lookup(jndiName);
        } catch (Exception e) {
            CoreSharedUtils.getLogger().severe("Error. Bean '" + jndiName + "' not found!");
            e.printStackTrace();
        }
        return null;
    }
    …
}



Важные замечания
Сегодня один умный человек с Хабра, который по каким-то причинам не может писать комментарии, заметил важную проблему, на которую стоит обратить внимание.
Поток может быть завершен из вне JVM, в этом случае блок finaly не будет выполнен. Гипотетически это может привести к утечке памяти, но меня данный вариант устраивает.


На этом у меня всё. Если у кого-то есть более интересные решения или критика — добро пожаловать в комментарии.

Комментарии (8)


  1. Borz
    18.01.2016 18:14

    SystemUtils — это чей класс? можно в код добавить import для классов?


    1. Neki
      18.01.2016 19:38

      Не заметил кнопку ответить. Ответил ниже.


  1. Neki
    18.01.2016 19:35

    SystemUtils свой класс. Добавил код под спойлер.


  1. ivnik
    19.01.2016 11:45
    +2

    private static ConcurrentHashMap<Long, Long> threadToPersonID = new ConcurrentHashMap<Long, Long>();

    Лучше использовать ThreadLocal:
    ThreadLocal<Long> userId = = new ThreadLocal<>()


    1. Neki
      19.01.2016 12:08

      Да точно, спасибо! Не знал про ThreadLocal. Искал нечто подобное, но так и не нашел. Очень здорово, теперь синхронизация точно не будет тормозить работу, при активном взаимодействии с БД. Вечером поправлю код в статье.


  1. Neki
    19.01.2016 21:47

    Сегодня один умный человек с Хабра, который по каким-то причинам не может писать комментарии, заметил две важные проблемы, которые стоит иметь ввиду.

    1) Стоит иметь ввиду, что в web приложениях используется пул потоков — потоки не уникальны. Данное решение будет работать, но только за счёт того что ThreadLocal каждый раз инициализируется заново.

    2) Если Thread получит interrupt — блок finally не выполнится. Если работу с базой данных всегда производить через FakeOwnerTransaction.start() — всё будет хорошо, иначе getFakeChanger() может вернуть неверное значение вместо null. Конечно вероятность ошибки крайне мала, но необходимо понимать её возможность.

    Добавил замечания в шапку.


    1. ivnik
      20.01.2016 14:04
      +1

      1 — не совсем понятен комментарий.

      Решение не будет работать если не инициализировать ThreadLocal и будет если инциализировать, и это не связано с тем откуда у нас тред. То что у нас повсеместно треды переиспользуются, означает что крайне важно сбрасывать ThreadLocal когда он не нужен (и с точки зрения корректности и с точки зрения memory leak-ов).

      2 — не верно.

      С interrupted-тредами возможны два состояния
      1) у треда стоит статус «interrupted»
      2) выпал «InterruptedException» (или подобное исключение)

      В первом случае с потоком исполнения ничего не происходит пока не произойдет вызов некоторых методов или блокирующего i/o. Вообще говоря тред может и не заметить что он «прерван» до вызова определенных методов. И в таком случае, после завершения try блока, произойдет нормальное исполнение finally блока.


      1. Neki
        20.01.2016 19:17

        Абсолютно верно. Спасибо за подробное разъяснение. По первому — просто неудачно выразился. По поводу interrupted действительно не правильно сказал, меня самого запутали. Поток может быть завершен извне JVM, в этом случае блок finaly не будет выполнен. Гипотетически это может привести к утечке памяти, но меня данное решение устраивает.