Фоновый процесс в приложении Vaadin 8

В моем приложении Vaadin 8, работающем на Tomcat, должен быть фоновый процесс для обновления и обновления базы данных. Если я использую ServletContextListener, чтобы отделить его от основного пользовательского интерфейса, Tomcat не завершит запуск, пока не завершит выполнение всех инструкций в contextInitialized, и поскольку я хочу сохранить его в бесконечном цикле в отдельном потоке, который вызывает базу данных, а затем спит в течение 5 минут, приложение никогда не запускается. Каким будет правильный способ реализовать это?


person Eight Rice    schedule 20.09.2019    source источник


Ответы (2)


Если не обновить пользовательский интерфейс в Vaadin

Ваш вопрос помечен как Vaadin, но, похоже, он касается только запуска фоновой задачи без учета пользовательского интерфейса Vaadin. Если это так, вы задаете общий вопрос Jakarta Servlet, а не вопрос, специфичный для Vaadin. Vaadin — это просто сервлет, хотя и очень большой и сложный сервлет.

Как вы заметили, написание класса, реализующего ServletContextListener — это место для запуска кода при запуске вашего веб-приложения до обслуживания первого пользователя в contextInitialized. И это место для запуска кода при выходе из вашего веб-приложения после обслуживания последнего пользователя в contextDestroyed.

После написания слушателя вы должны сообщить контейнеру сервлетов (например, Apache Tomcat или Eclipse Jetty) о его существовании. Самый простой способ сделать это — добавить @WebListener< /a> аннотация.

package com.example.acme;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

/**
 *
 * @author Basil Bourque
 */
@WebListener
public class AcmeServletContextListener implements ServletContextListener {

    @Override
    public void contextInitialized ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is starting. " );
    }

    @Override
    public void contextDestroyed ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is exiting." );
    }

}

Не запускайте фоновую задачу с помощью Thread. Это старая школа. Современный подход использует структуру Executor, добавленную позже в Java. См. учебник по Oracle.

ExecutorService

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

ExecutorService executorService = Executors.newSingleThreadExecutor() ;

Определите свою задачу как Runnable или Callable.

Runnable runnable = new Runnable ()
{
    @Override
    public void run ( )
    {
        System.out.println ( "INFO - Acme web app doing some work on background thread. " + Instant.now () );
    }
};

Вы можете использовать более компактный синтаксис лямбда для определения вашего Runnable, если хотите. Я использовал здесь длинный синтаксис для ясности.

Скажите службе-исполнителю запустить этот исполняемый файл.

executorService.submit ( runnable );

A Future объект возвращается как дескриптор для проверки хода выполнения или завершения задачи. Вы можете не использовать его.

Future future = executorService.submit ( runnable );

Соберите все это вместе, а также код для корректного закрытия нашего пула потоков (службы-исполнителя). И мы добавляем временные метки в наши консольные сообщения с помощью Instant.now().

public class AcmeServletContextListener implements ServletContextListener {
    private ExecutorService executorService ; 

    @Override
    public void contextInitialized ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is starting. " + Instant.now () );
        this.executorService = Executors.newSingleThreadExecutor() ;
        Runnable runnable = new Runnable ()
        {
            @Override
            public void run ( )
            {
                System.out.println ( "INFO - Acme web app doing some work on background thread. " + Instant.now () );
            }
        };
        Future future = this.executorService.submit ( runnable );
    }

    @Override
    public void contextDestroyed ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is exiting. " + Instant.now () );
        if ( Objects.nonNull ( executorService ) )
        {
            this.executorService.shutdown ();
        }
    }

}

ScheduledExecutorService

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

Где заниматься планированием? В сервисе-исполнителе, специально созданном для этой работы. Реализация ScheduledExecutorService имеет методы для однократного запуска задачи с задержкой или без нее (период ожидания). Или вы можете вызвать методы, чтобы запланировать повторение задачи, скажем, каждые пять минут.

Код, аналогичный приведенному выше. Меняем ExecutorService на ScheduledExecutorService. И меняем Executors.newSingleThreadExecutor() на Executors.newSingleThreadScheduledExecutor(). Мы указываем начальную задержку и период повторения, используя TimeUnit перечисление. Здесь мы используем TimeUnit.MINUTES с начальной задержкой 2 (подождите две минуты перед первым запуском) и периодом каждые пять минут. Если вы хотите использовать Future, теперь это ScheduledFuture.

public class AcmeServletContextListener implements ServletContextListener {
    private ScheduledExecutorService executorService ; 

    @Override
    public void contextInitialized ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is starting. " + Instant.now () );
        // Instantiate a thread pool and scheduler.
        this.executorService = Executors.newSingleThreadScheduledExecutor() ;
        // Define the task to be done.
        Runnable runnable = new Runnable ()
        {
            @Override
            public void run ( )
            {
                System.out.println ( "INFO - Acme web app doing some work on background thread. " + Instant.now () );
            }
        };
        // Tell the scheduler to run the task repeatedly at regular intervals, after an initial delay. 
        ScheduledFuture future = this.executorService.scheduleAtFixedRate​ ( runnable , 2 , 5 , TimeUnit.MINUTES );
    }

    @Override
    public void contextDestroyed ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is exiting. " + Instant.now () );
        if ( Objects.nonNull ( executorService ) )
        {
            this.executorService.shutdown ();
        }
    }

}

Важный совет. Оберните свою работу в Runnable внутри try-catch, чтобы поймать любые Exception, которые могут всплыть. Если исключение (или Error) достигнет запланированной службы исполнителя, эта служба остановит все дальнейшие выполнения. Ваша фоновая задача перестает работать, тихо, загадочно. Лучше перехватывать все непредвиденные исключения (и, возможно, ошибки, это спорно) и сообщать системному администратору, если для вас важно, чтобы фоновая задача продолжалась.

Джакартский параллелизм

Если вы выполняете развертывание на сервере приложений с поддержкой утилит Jakarta Concurrency (изначально JSR 236), эта работа становится намного проще. Вам не нужно писать, что ServletContextListener. Вы можете использовать аннотации, чтобы сервер приложений автоматически запускал ваш файл Runnable.

При обновлении пользовательского интерфейса в Vaadin

Возможно, вам нужен один фоновый рабочий процесс, который каждые пять минут обновляет отображение некоторых ваших пользователей. Если это так, вам нужно несколько частей:

  • Фоновый поток выполняет некоторую периодическую работу,
  • Реестр просмотров пользователей, который будет обновляться,
  • Многие экземпляры представления (макета или виджета Vaadin) заинтересованы в обновлении.

Другими словами, ограниченная форма Pub-Sub, шаблон издателя и подписчика только с одним издателем.

Реализация ServletContextListener — это один из способов выполнения работы при запуске веб-приложения Vaadin (до обслуживания каких-либо пользователей) и при выходе из веб-приложения (после обслуживания последнего пользователя). Это хорошее место для запуска и закрытия вашего издательства pub-sub и реестра.

Вы можете сохранить ссылку на свой объект реестра по всему миру в контексте сервлета, отправленном в вашу реализацию ServletContextListener. Используйте функцию «атрибуты», коллекцию ключей и значений, доступ к которой осуществляется с помощью методов setAttribute/getAttribute/removeAttribute.

Если ваш фоновый рабочий процесс выполняется с перерывами, а не непрерывно, узнайте о структуре исполнителей, в частности о файле ScheduledExecutorService. Не забудьте корректно закрыть любую такую ​​службу-исполнитель, поскольку она может пережить ваше веб-приложение и даже ваш контейнер Serlet! При использовании полноценного сервера Jakarta EE (например, Glassfish/Payara, WildFly, и тому подобное), а не просто контейнер сервлетов (например, Apache Tomcat или Eclipse Jetty), вы можете использовать Функция Concurrency Utilities, упрощающая запуск управляемого исполнителя по расписанию с автоматическим запуском/остановкой.

Когда вы создаете экземпляр представления для обновления в своем пользовательском интерфейсе Vaadin, зарегистрируйте этот макет или виджет в реестре как заинтересованный в получении обновлений. Получите реестр из ServletContext веб-приложения, как обсуждалось. в разделе Различные способы получения контекста сервлета.

Я предлагаю вашему реестру сохранять слабые ссылки на заинтересованные взгляды. Каждое из этих представлений в конечном итоге исчезнет, ​​когда пользователь закроет окно/вкладку веб-браузера. Вы можете запрограммировать зарегистрированный виджет на изящную отмену регистрации в реестре в рамках его жизненного цикла. Но я подозреваю, что использование слабых ссылок поможет сделать это надежным. Одна из возможностей — использовать WeakHashMap только с ключами и без значений, где каждый ключ является слабой ссылкой на экземпляр вашего виджета/макета, зарегистрированного для обновлений из фонового потока.

Чтобы фоновый поток обновлял пользовательский интерфейс веб-приложения Vaadin, никогда не обращайтесь к виджетам Vaadin из фонового потока. Сначала может показаться, что это работает, но в конечном итоге вы столкнетесь с конфликтом concurrency. и очень плохие вещи могут последовать. Вместо этого узнайте, как Vaadin позволяет отправить запрос на обновление с помощью метода access, передав Runnable. Кроме того, вы захотите узнать о технологии push-уведомлений и о том, как Vaadin делает Push очень простым. Обратите внимание на раздел на этой странице: Вещание другим пользователям, которое описывает то же самое, что и этот ответ. Попутно вы, вероятно, узнаете о преимуществах и ограничениях WebSockets, которые можно использовать автоматически библиотекой Atmosphere, используемой Vaadin для реализации Push.

На протяжении всего этого вы должны очень внимательно относиться к проблемам и практикам параллелизма и, возможно, к ключевому слову volatile. Контейнер сервлетов Java по определению представляет собой среду с большим количеством потоков, и теперь вы будете выполнять собственную хореографию этих потоков. Поэтому вам нужно будет читать, перечитывать и усердно изучать прекрасную книгу Java Параллелизм на практике, Брайан Гетц и др.

Написав все это, я понимаю, что теперь ваш вопрос слишком широк для переполнения стека. Но, надеюсь, этот ответ поможет вам сориентироваться. Вы можете узнать больше о каждой части головоломки, выполнив поиск в Stack Overflow. В частности, если вы выполните поиск в Stack Overflow, вы найдете несколько очень длинных сообщений на эти самые темы с большим количеством примеров кода в Vaadin 8. И обратитесь к Форумы Vaadin. Если это жизненно важный проект с финансированием, рассмотрите возможность найма на обучение и консультационные услуги, доступные в компании Vaadin Ltd. Ваш проект выполним; Я сам сделал такой проект примерно в том же духе, что и здесь. Это не просто, но возможно, и это довольно интересная работа.

person Basil Bourque    schedule 21.09.2019
comment
Большое спасибо за исчерпывающий ответ, Василий. На самом деле мне не нужно обновлять активные сеансы в режиме реального времени. Каждый сеанс может использовать состояние базы данных, доступное на момент его запуска, и пользователь может обновить страницу, чтобы получить обновленную версию. Снижает ли это сложность реализации? - person Eight Rice; 21.09.2019
comment
@AndreiȚăranu Тогда гораздо проще, подмножество того, что я описал. В вашей реализации ServletContextListener создайте службу-исполнитель. Сохраните ссылку на него как на поле-член. Используйте эту службу-исполнитель для запуска фоновой задачи как Runnable в другом потоке. Вуаля, ветка вашего слушателя может продолжаться до завершения. В методе contextDestroyed вашего слушателя используйте сохраненную ссылку на службу-исполнитель, чтобы закрыть ее при выходе из вашего веб-приложения. - person Basil Bourque; 21.09.2019

Причина, по которой Tomcat ждал завершения процесса, заключается в том, что я использовал thread.run() вместо thread.start().

person Eight Rice    schedule 21.09.2019
comment
Лучше избегать ручного использования Thread, особенно на сервере приложений. Я добавил в свой ответ длинный раздел, показывающий, как использовать Executor, чтобы легко и изящно обрабатывать ваши потоки. - person Basil Bourque; 22.09.2019