Безопасно ли запустить новый поток в управляемом JSF bean?

Я не смог найти окончательного ответа на вопрос о том, безопасно ли появляться потоки в управляемом JSF-интерфейсе с поддержкой сеанса beans. Поток должен вызывать методы для экземпляра EJB без сохранения состояния (который был связан с зависимостями с управляемым bean).

Предполагается, что у нас есть отчет, который требует много времени для генерации. Это вызвало запрос HTTP на тайм-аут из-за настроек сервера, которые мы не можем изменить. Поэтому идея состоит в том, чтобы начать новый поток и позволить ему генерировать отчет и временно хранить его. В то же время на странице JSF отображается индикатор выполнения, опрос управляемых bean до завершения генерации, а затем второй запрос на загрузку сохраненного отчета. Кажется, это работает, но я хотел бы быть уверенным, что я делаю это не взлом.

Ответ 1

Введение

Нерезидентные потоки из управляемого сеанса bean не обязательно взломаны, если он выполняет задание, которое вы хотите. Но нерестовые потоки при необходимости должны выполняться с особой осторожностью. Код не должен записываться таким образом, чтобы один пользователь мог, например, создать неограниченное количество потоков за сеанс и/или продолжить работу потоков даже после того, как сеанс будет уничтожен. Это рано или поздно взорвет ваше выражение.

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

Кроме того, все эти потоки должны предпочтительно обслуживаться общим пулом потоков, так что вы можете ограничить общее количество порожденных потоков на уровне приложения. Средний сервер приложений Java EE предлагает пул потоков, управляемый контейнером, который вы можете использовать, среди прочих, EJB @Asynchronous и @Schedule. Чтобы быть независимым от контейнера, вы также можете использовать Java 1.5 Util Concurrent ExecutorService и ScheduledExecutorService для этого.

Ниже приведены примеры Java EE 6+ с EJB.

Огонь и забудьте задачу на форме submit

@Named
@RequestScoped // Or @ViewScoped
public class Bean {

    @EJB
    private SomeService someService;

    public void submit() {
        someService.asyncTask();
        // ... (this code will immediately continue without waiting)
    }

}
@Stateless
public class SomeService {

    @Asynchronous
    public void asyncTask() {
        // ...
    }

}

Асинхронно извлекает модель при загрузке страницы

@Named
@RequestScoped // Or @ViewScoped
public class Bean {

    private Future<List<Entity>> asyncEntities;

    @EJB
    private EntityService entityService;

    @PostConstruct
    public void init() {
        asyncEntities = entityService.asyncList();
        // ... (this code will immediately continue without waiting)
    }

    public List<Entity> getEntities() {
        try {
            return asyncEntities.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new FacesException(e);
        } catch (ExecutionException e) {
            throw new FacesException(e);
        }
    }
}
@Stateless
public class EntityService {

    @PersistenceContext
    private EntityManager entityManager;

    @Asynchronous
    public Future<List<Entity>> asyncList() {
        List<Entity> entities = entityManager
            .createQuery("SELECT e FROM Entity e", Entity.class)
            .getResultList();
        return new AsyncResult<>(entities);
    }

}

Если вы используете служебную библиотеку JSF OmniFaces, это можно сделать еще быстрее, если вы аннотируете управляемый bean с помощью @Eager.

Расписание фоновых заданий при запуске приложения

@Singleton
public class BackgroundJobManager {

    @Schedule(hour="0", minute="0", second="0", persistent=false)
    public void someDailyJob() {
        // ... (runs every start of day)
    }

    @Schedule(hour="*/1", minute="0", second="0", persistent=false)
    public void someHourlyJob() {
        // ... (runs every hour of day)
    }

    @Schedule(hour="*", minute="*/15", second="0", persistent=false)
    public void someQuarterlyJob() {
        // ... (runs every 15th minute of hour)
    }

    @Schedule(hour="*", minute="*", second="*/30", persistent=false)
    public void someHalfminutelyJob() {
        // ... (runs every 30th second of minute)
    }

}

Постоянно обновлять модель приложения в фоновом режиме

@Named
@RequestScoped // Or @ViewScoped
public class Bean {

    @EJB
    private SomeTop100Manager someTop100Manager;

    public List<Some> getSomeTop100() {
        return someTop100Manager.list();
    }

}
@Singleton
@ConcurrencyManagement(BEAN)
public class SomeTop100Manager {

    @PersistenceContext
    private EntityManager entityManager;

    private List<Some> top100;

    @PostConstruct
    @Schedule(hour="*", minute="*/1", second="0", persistent=false)
    public void load() {
        top100 = entityManager
            .createNamedQuery("Some.top100", Some.class)
            .getResultList();
    }

    public List<Some> list() {
        return top100;
    }

}

См. также:

Ответ 2

Проверьте EJB 3.1 @Asynchronous methods. Это именно то, для чего они предназначены.

Небольшой пример использования OpenEJB 4.0.0-SNAPSHOT. Здесь мы имеем @Singleton bean с одним методом, отмеченным @Asynchronous. Каждый раз, когда этот метод вызывается кем угодно, в этом случае ваш JSF управляется bean, он немедленно возвращается независимо от того, как долго этот метод действительно принимает.

@Singleton
public class JobProcessor {

    @Asynchronous
    @Lock(READ)
    @AccessTimeout(-1)
    public Future<String> addJob(String jobName) {

        // Pretend this job takes a while
        doSomeHeavyLifting();

        // Return our result
        return new AsyncResult<String>(jobName);
    }

    private void doSomeHeavyLifting() {
        try {
            Thread.sleep(SECONDS.toMillis(10));
        } catch (InterruptedException e) {
            Thread.interrupted();
            throw new IllegalStateException(e);
        }
    }
}

Вот небольшой тестовый файл, который вызывает этот метод @Asynchronous несколько раз подряд.

Каждый вызов возвращает объект Future, который по сути начинается пустым и позже будет иметь значение, заполненное контейнером, когда фактически завершается вызов соответствующего метода.

import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class JobProcessorTest extends TestCase {

    public void test() throws Exception {

        final Context context = EJBContainer.createEJBContainer().getContext();

        final JobProcessor processor = (JobProcessor) context.lookup("java:global/async-methods/JobProcessor");

        final long start = System.nanoTime();

        // Queue up a bunch of work
        final Future<String> red = processor.addJob("red");
        final Future<String> orange = processor.addJob("orange");
        final Future<String> yellow = processor.addJob("yellow");
        final Future<String> green = processor.addJob("green");
        final Future<String> blue = processor.addJob("blue");
        final Future<String> violet = processor.addJob("violet");

        // Wait for the result -- 1 minute worth of work
        assertEquals("blue", blue.get());
        assertEquals("orange", orange.get());
        assertEquals("green", green.get());
        assertEquals("red", red.get());
        assertEquals("yellow", yellow.get());
        assertEquals("violet", violet.get());

        // How long did it take?
        final long total = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start);

        // Execution should be around 9 - 21 seconds
        assertTrue("" + total, total > 9);
        assertTrue("" + total, total < 21);
    }
}

Пример исходного кода

Под обложками работает эта работа:

  • JobProcessor вызывающий объект на самом деле не является экземпляром JobProcessor. Скорее это подкласс или прокси-сервер, у которого все методы переопределены. Методы, которые должны быть асинхронными, обрабатываются по-разному.
  • Вызовы асинхронного метода просто приводят к созданию Runnable, которое обертывает метод и параметры, которые вы указали. Этот runnable присваивается Executor, который является просто рабочей очередью, прикрепленной к пулу потоков.
  • После добавления работы в очередь проксированная версия метода возвращает реализацию Future, которая связана с Runnable, которая теперь ожидает очередь.
  • Когда Runnable, наконец, выполнит метод реального экземпляра JobProcessor, он примет возвращаемое значение и установит его в Future, сделав его доступным для вызывающего.

Важно отметить, что объект AsyncResult, возвращаемый JobProcessor, представляет собой не тот объект Future, который удерживает вызывающий объект. Было бы аккуратно, если бы реальный JobProcessor мог просто вернуть String, а версия вызывающего JobProcessor могла возвращать Future<String>, но мы не видели никакого способа сделать это, не добавляя больше сложности. Таким образом, AsyncResult - это простой объект-оболочка. Контейнер вытащит String, отбросит AsyncResult, а затем положит String в реальный Future, который удерживает вызывающий.

Чтобы добиться прогресса на этом пути, просто передайте потокобезопасный объект, например AtomicInteger, в метод @Asynchronous и периодически обновляйте код bean он с процентом завершен.

Ответ 3

Я пробовал это и отлично работал с моим управляемым JSF bean

ExecutorService executor = Executors.newFixedThreadPool(1);

@EJB
private IMaterialSvc materialSvc;

private void updateMaterial(Material material, String status,  Location position) {

    executor.execute(new Runnable() {
        public void run() {
            synchronized (position) {
                // TODO update material in audit? do we need materials in audit?
                int index = position.getMaterials().indexOf(material);
                Material m = materialSvc.getById(material.getId());
                m.setStatus(status);
                m = materialSvc.update(m);
                if (index != -1) {
                    position.getMaterials().set(index, m);
                }

            }
        }
    });

}

@PreDestroy
public void destory() {
    executor.shutdown();
}