В моем приложении Vaadin 8, работающем на Tomcat, должен быть фоновый процесс для обновления и обновления базы данных. Если я использую ServletContextListener, чтобы отделить его от основного пользовательского интерфейса, Tomcat не завершит запуск, пока не завершит выполнение всех инструкций в contextInitialized, и поскольку я хочу сохранить его в бесконечном цикле в отдельном потоке, который вызывает базу данных, а затем спит в течение 5 минут, приложение никогда не запускается. Каким будет правильный способ реализовать это?
Фоновый процесс в приложении Vaadin 8
Ответы (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. Ваш проект выполним; Я сам сделал такой проект примерно в том же духе, что и здесь. Это не просто, но возможно, и это довольно интересная работа.
ServletContextListener
создайте службу-исполнитель. Сохраните ссылку на него как на поле-член. Используйте эту службу-исполнитель для запуска фоновой задачи как Runnable
в другом потоке. Вуаля, ветка вашего слушателя может продолжаться до завершения. В методе contextDestroyed
вашего слушателя используйте сохраненную ссылку на службу-исполнитель, чтобы закрыть ее при выходе из вашего веб-приложения.
- person Basil Bourque; 21.09.2019
Причина, по которой Tomcat ждал завершения процесса, заключается в том, что я использовал thread.run() вместо thread.start().
Thread
, особенно на сервере приложений. Я добавил в свой ответ длинный раздел, показывающий, как использовать Executor, чтобы легко и изящно обрабатывать ваши потоки.
- person Basil Bourque; 22.09.2019