Статьи

Длинный опрос с Spring 3.2’s DeferredResult

В нашем последнем эпизоде генеральный директор Agile Cowboys Inc только что нанял консультанта по Java / Spring, дав ему Porsche, который он изначально купил для своей подруги. Расстроенная потерей своего приза Porsche, девушка генерального директора рассказала своей жене об их романе. Его жена после нарезки свиты генерального директора подала на развод. Тем временем генеральный директор внедрил новый «повседневный» дресс-код в офисе, и консультант по Java / Spring только что вернулся из своего нового Porsche и садится за стол, собираясь починить программное обеспечение телекомпании… Если это не так ничего для вас не значит, тогда взгляните на Long Polling Tomcat With Spring .

Консультант по Java / Spring должен решить проблему с ресурсами сервера телекомпании перед следующей крупной игрой, и он знает, что может сделать это, внедрив технику Spring Deferred Result с использованием спецификации Servlet 3, реализованной в Tomcat 7 1

Первое, что делает консультант по Java / Spring, это проверяет файл проекта pom.xml . Для асинхронного проекта Servlet 3 необходимо включить следующую зависимость:

1
2
3
4
5
6
<dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
        <scope>provided</scope>
    </dependency>

Далее вы должны сообщить Tomcat, что Spring DispatcherServlet поддерживает Servlet 3 асинхронную связь. Это достигается добавлением следующей строки в ваш web.xml :

1
<async-supported>true</async-supported>

Полная конфигурация DispatcherServlet :

01
02
03
04
05
06
07
08
09
10
<servlet>
 <servlet-name>appServlet</servlet-name>
 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
 <init-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
 </init-param>
 <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>

Разобравшись с конфигурацией проекта, Java / Spring Consultant быстро переходит к коду контроллера. Он заменяет SimpleMatchUpdateController на новый DeferredMatchUpdateController :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller()
public class DeferredMatchUpdateController {
 
  @Autowired
  private DeferredResultService updateService;
 
  @RequestMapping(value = "/matchupdate/begin" + "", method = RequestMethod.GET)
  @ResponseBody
  public String start() {
    updateService.subscribe();
    return "OK";
  }
 
  @RequestMapping("/matchupdate/deferred")
  @ResponseBody
  public DeferredResult<Message> getUpdate() {
 
    final DeferredResult<Message> result = new DeferredResult<Message>();
    updateService.getUpdate(result);
    return result;
  }
}

Новый DeferredMatchUpdateController довольно прост. Как и SimpleMatchUpdateController он содержит два метода: start() и getUpdate() , которые выполняют ту же работу, что и их простые аналоги. Это делает этот контроллер заменой плагина для SimpleMatchUpdateController . Большая разница в том, что методы getUpdate() создают экземпляр Spring’s DeferredResult , который он передает новому DeferredResultService перед возвратом его в Spring. Затем Spring паркует HTTP-запрос, позволяя ему зависнуть, пока у объекта DeferredResult не будет данных для возврата в браузер.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Service("DeferredService")
public class DeferredResultService implements Runnable {
 
  private static final Logger logger = LoggerFactory.getLogger(DeferredResultService.class);
 
  private final BlockingQueue<DeferredResult<Message>> resultQueue = new LinkedBlockingQueue<>();
 
  private Thread thread;
 
  private volatile boolean start = true;
 
  @Autowired
  @Qualifier("theQueue")
  private LinkedBlockingQueue<Message> queue;
 
  @Autowired
  @Qualifier("BillSkyes")
  private MatchReporter matchReporter;
 
  public void subscribe() {
    logger.info("Starting server");
    matchReporter.start();
    startThread();
  }
 
  private void startThread() {
 
    if (start) {
      synchronized (this) {
        if (start) {
          start = false;
          thread = new Thread(this, "Studio Teletype");
          thread.start();
        }
      }
    }
  }
 
  @Override
  public void run() {
 
    while (true) {
      try {
 
        DeferredResult<Message> result = resultQueue.take();
        Message message = queue.take();
 
        result.setResult(message);
 
      } catch (InterruptedException e) {
        throw new UpdateException("Cannot get latest update. " + e.getMessage(), e);
      }
    }
  }
 
  public void getUpdate(DeferredResult<Message> result) {
    resultQueue.add(result);
  }
 
}

Опять же, как и его аналог SimpleMatchUpdateService DeferredResultService содержит два метода: subscribe() и getUpdate()

Имея дело с getUpdate(...) , все это происходит для добавления вновь созданного объекта DeferredResult в LinkedBlockingQueue именем resultQueue , чтобы с ним можно было справиться позже, когда будет доступно обновление соответствия.

Реальная работа выполняется методом subscribe() . Во-первых, этот метод запускает matchReporter , который в соответствующий момент передает обновления совпадений в экземпляр queue с автоматической проводной связью. Затем он вызывает закрытый startThread() чтобы запустить рабочий поток. Это запускается только один раз и использует двойную проверку блокировки, чтобы гарантировать, что это делается эффективно и без проблем.

Метод run() потока бесконечно зацикливается, сначала беря объект DeferredResult из resultQueue , если он доступен, а затем объект Message , представляющий обновление совпадения из queue обновлений, снова, если доступно. Затем он вызывает setResult(...) DeferredResult используя объект message в качестве аргумента. Spring вступит во владение, и оригинальный длинный запрос будет завершен, а данные запоздало возвращены в браузер.

Обратите внимание, что в этом примере кода метод run() содержит цикл while(true) . Хотя этот метод упрощает пример кода, это не очень хорошая идея, когда речь идет о производственном коде. Одной из проблем использования своенравных, неконтролируемых потоков является то, что они перестают корректно завершать работу Tomcat, и вам обычно нужно использовать хорошую команду Unix kill для остановки вашего сервера. В производственном коде хорошей идеей будет включить код для изящного закрытия рабочих потоков.

После тяжелой пары часов работы консультант по Java / Spring продвигает свой код на жизнь, подбирает ключи к Porsche и начинает вращаться. В следующую субботу, используя Spring DeferredResult , серверы прекрасно справляются: пользователи счастливы, президент телекомпании счастлив, а генеральный директор Agile Cowboys Inc счастлив, хотя у него есть подозрение, что он слишком много заплатил консультанту, но эй, это только деньги.

1 При написании этого блога я использовал Tomcat версии 7.0.42

Код, сопровождающий этот блог, доступен на Github по адресу: https://github.com/roghughe/captaindebug/tree/master/long-poll