Author Archives: elwood
При разработке многопоточных приложений часто применяется следующий паттерн для синхронизации потоков : создается массив сигнализирующих объектов и вызывается wait для них всех, который блокирует выполнение потока и ожидает, пока один из этих объектов не даст сигнал. В это время другие потоки что-то делают, и рано или поздно сигнал вызывается. После этого wait завершает ожидание, возвращая индекс сработавшего объекта-события. Далее производятся какие-то действия по обработке этого конкретного события, и wait вызывается снова. В псевдокоде это выглядит примерно так:
event[] events = initializeEvents(); while (true) { int index = wait(events, TIMEOUT); if (0 == index) { // wait finished by timeout } else if (-1 == index) { // wait finished with error } else { event signalledEvent = events[index]; // todo : working with signalled event if (signalledEvent == EXIT_EVENT) { break; } } } |
Это очень удобный паттерн, который можно применять как в синхронизации потоков (различные очереди сообщений или просто разные действия), так и в обработке ввода (в Windows функции ожидания можно применить к STD_INPUT_HANDLE, поместив STD_INPUT_HANDLE в один массив с другими объектами-событиями).
И вот мне нужно было сделать аналогичное решение для обработки консольного ввода в Linux.
Была найдена функция poll(), которая работает с массивом файловых дескрипторов. Однако, мьютекс в этот массив засунуть нельзя, а отдельный файл или pipe создавать для этого дела не хочется. Должен же быть некий объект ядра, который будет достаточно легковесным для использования в этом сценарии ! И действительно, такой объект нашелся. Им оказался eventfd – сравнительно недавно появившийся в Linux API (первая версия появилась в ядре 2.6, а последние изменения в API были добавлены в версии 2.6.30).
Итак, как же работает eventfd. Обычный объект eventfd (не-семафорный, то есть не созданный с флагом EFD_SEMAPHORE) работает достаточно просто. Он содержит в себе 64-битный счетчик (изначально равный нулю) и позволяет писать в себя вызовом write() и читать вызовом read(). При записи (поддерживается только запись 64-битного значения) записываемое значение добавляется к счетчику. То есть вызвали 3 раза запись числа 10 – счетчик будет содержать значение 30. При вызове read() это значение будет считано, а счётчик вновь сброшен в ноль. Когда счетчик содержит 0, вызов poll с флагом POLLIN будет ожидать до тех пор, пока кто-то не запишет в eventfd значение. Если же счетчик содержит 1, вызов poll с флагом POLLIN вернёт управление сразу (раз есть, что считывать – eventfd “доступен для чтения”). Причем, так как при вызове poll() счётчик не сбрасывается, повторные вызовы будут тут же возвращать управление до тех пор, пока кто-то не вызовет read() и не сбросит тем самым значение внутреннего счетчика в ноль. Вызов же poll() с флагом POLLOUT будет, как правило, всегда возвращать true для флага POLLOUT (поскольку запись в него в принципе всегда разрешена, за исключением сценариев, при которых eventfd может быть заблокирован из-за другого активного в этот момент писателя).
Ну и так как eventfd представляет собой файловый дескриптор, связанный с объектом ядра, то его нужно закрывать вызовом close().
Итак, чтобы сделать аналогичную схему, нам достаточно простого алгоритма :
int fd = eventfd(0, EFD_CLOEXEC); // пусть это событие сигнализирует нам о продолжении некой работы int fd2 = eventfd(0, EFD_CLOEXEC); // а это - о необходимости выйти из цикла приёма сообщений pollfd pollfds[2]; pollfds[0].fd = fd; pollfds[1].fd = fd2; pollfds[0].events = POLLIN; pollfds[1].events = POLLIN; bool exitFlag = false; while (!exitFlag) { int pollResult = poll(pollfds, 2, -1); if (pollResult == -1) { // handle error } else if (pollResult > 0) { // сработало одно или несколько событий - но без проверки revents - // флагов событий, действительно сработавших - нам не узнать индексы сработавших событий for (int i = 0; i < 2; i++) { if (pollfds[i].revents != 0) { if (i == 0) { // делаем работу, относящуюся к первому событию } else if (i == 1) { exitFlag = true; break; } // обнуляем внутренний счетчик, чтобы следующий вызов poll снова ожидал записи в eventfd uint64_t u; read(pollfds[i].fd, &u, sizeof(uint64_t)); } } } } ... // а тут где-то в другом потоке вызываем сигналы вот так uint64_t u = 1; write(fd, &u, sizeof(uint64_t)); |
При необходимости можно усовершенствовать схему, заменив третий аргумент poll с -1 (TIMEOUT_INDEFINITE) на конкретный таймаут и добавив обработку того случая, когда pollResult равен нулю (что как раз соответствует возврату по таймауту).
В общем, мы видим, что в принципе, ничего сложного в синхронизации через файловые дескрипторы нет, и можно легко построить цикл обработки сообщений из других потоков / процессов по аналогии с тем, как это привыкли делать в Windows с помощью объектов ядра Event и функций WaitForSingleObject / WaitForMultipleObjects.
PS. Возможно, производительность этого способа может быть хуже, чем при использовании мьютексов. Поэтому если производительность критична, наверное, стоит задуматься о том, как реализовать аналогичное поведение на POSIX мьютексах. На гитхабе есть проект https://github.com/NeoSmart/PEvents – он как раз про это (правда, там довольно много кода по сравнению с приведённым сниппетом).
С недавних пор я отказался от хитрых способов для реализации ajax-запросов (таких, как DWR, к примеру), отдав предпочтение обычным спринговым обработчикам, помеченным аннотацией @ResponseBody. Настроил конвертеры для json и xml, добавив их в список конвертеров AnnotationMethodHandlerAdapter. Но, как оказалось, настроил я не всё.
Дело в том, что обычно если мне нужно было получить кусок разметки при помощи ajax, я не выставлял @ResponseBody (поскольку это был “обычный” запрос), а в методе возвращал название view. Spring брал по названию view нужный jsp (хотя у меня не напрямую jsp, а через Tiles, что не суть важно). Однако мне вдруг понадобилось напрямую вернуть String. И как оказалось, конвертер StringHttpMessageConverter упрямо перекодирует возвращаемую мной строку в ISO-8859-1, свою дефолтную кодировку. Причем ни выставленные Accepts и Accept-Charset заголовки, ни указанное в @RequestMapping‘e свойство produce=”text/html;charset=UTF-8″, ни сконфигурированные в конвертере supportedMediaTypes не влияли на результат.
Полез дебагером внутрь конвертера, где выяснил, что действительно, это всё не влияет на определение нужной кодировки. А влияет лишь переданный в конвертер MediaType, который как правило содержал только mime-type, а кодировка была установлена в null. После чего конвертер, видя что ему пришел null, брал свою дефолтную кодировку и применял её к строке. Причем эту дефолтную кодировку нельзя задать ни в конструкторе конвертера, ни с помощью сеттера. Пришлось пойти на крайние меры – сделать форк конвертера в свой класс, добавить возможность задания дефолтной кодировки и заменить стандартный конвертер на свой в конфиге AnnotationMethodHandlerAdapter‘a.
Код:
/** * User: igor.kostromin * Date: 22.08.12 * Time: 12:11 * * Differs from StringHttpMessageConverter only in default charset specifying feature. */ public final class ConfigurableStringHttpMessageConverter extends AbstractHttpMessageConverter<String> { private Charset defaultCharset; public Charset getDefaultCharset() { return defaultCharset; } private final List<Charset> availableCharsets; private boolean writeAcceptCharset = true; public ConfigurableStringHttpMessageConverter() { super(new MediaType("text", "plain", StringHttpMessageConverter.DEFAULT_CHARSET), MediaType.ALL); defaultCharset = StringHttpMessageConverter.DEFAULT_CHARSET; this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values()); } public ConfigurableStringHttpMessageConverter(String charsetName) { super(new MediaType("text", "plain", Charset.forName(charsetName)), MediaType.ALL); defaultCharset = Charset.forName(charsetName); this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values()); } /** * Indicates whether the {@code Accept-Charset} should be written to any outgoing request. * <p>Default is {@code true}. */ public void setWriteAcceptCharset(boolean writeAcceptCharset) { this.writeAcceptCharset = writeAcceptCharset; } @Override public boolean supports(Class<?> clazz) { return String.class.equals(clazz); } @Override protected String readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); return FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset)); } @Override protected Long getContentLength(String s, MediaType contentType) { Charset charset = getContentTypeCharset(contentType); try { return (long) s.getBytes(charset.name()).length; } catch (UnsupportedEncodingException ex) { // should not occur throw new InternalError(ex.getMessage()); } } @Override protected void writeInternal(String s, HttpOutputMessage outputMessage) throws IOException { if (writeAcceptCharset) { outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); } Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType()); FileCopyUtils.copy(s, new OutputStreamWriter(outputMessage.getBody(), charset)); } /** * Return the list of supported {@link Charset}. * * <p>By default, returns {@link Charset#availableCharsets()}. Can be overridden in subclasses. * * @return the list of accepted charsets */ protected List<Charset> getAcceptedCharsets() { return this.availableCharsets; } private Charset getContentTypeCharset(MediaType contentType) { if (contentType != null && contentType.getCharSet() != null) { return contentType.getCharSet(); } else { return defaultCharset; } } } |
Использование:
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <util:list> <bean class="ru.dz.mvk.util.ConfigurableStringHttpMessageConverter"> <constructor-arg index="0" value="UTF-8"/> </bean> <bean class="org.springframework.http.converter.FormHttpMessageConverter"/> <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/> <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter"/> <bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter"/> <!-- json converter (for application/json multimedia type) --> <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/> <!-- xml converter (for application/xhtml+xml)--> <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter"> <property name="marshaller" ref="xstreamMarshaller"/> <property name="unmarshaller" ref="xstreamMarshaller"/> </bean> </util:list> </property> </bean> |
Часто бывает нужно добавлять в model какой-то глобальный объект (например, информацию о текущем пользователе). В каждом методе каждого контроллера делать это лениво. На помощь приходит механизм интерсепторов – объектов, которые перехватывают обработку запросов на различных этапах их жизненного цикла. Для создания собственного интерсептора достаточно занаследоваться от класса HandlerInterceptorAdapter (или напрямую от интерфейса HandlerInterceptor). А чтобы Spring увидел ваш интерсептор, нужно вписать его инстанс в конфиг вашего HandlerMapping’а, например, так:
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"> <property name="interceptors"> <list> <bean class="su.elwood.controllers.CommonInterceptor"/> </list> </property> </bean> |
При этом сам класс может выглядеть так:
public final class CommonInterceptor extends HandlerInterceptorAdapter { @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // modelAndView is null if @ResponseBody is used // look http://forum.springsource.org/showthread.php?93859-SessionAttributes-not-working-when-used-with-ResponseBody // for detailed explanation if (null != modelAndView) { // if modelAndView.getViewName() starts with "redirect:" we shouldn't add an attributes to // model because it will be placed in actual redirect URL String viewName = modelAndView.getViewName(); if (viewName != null && !viewName.startsWith("redirect:")) { User currentUser = MembershipHelper.getCurrentUser(); modelAndView.addObject("user", currentUser); } } } } |
Здесь нужно сделать важное замечание. Приведённое условие if (!modelAndView.getViewName().startsWith(“redirect:”)) просто необходимо для корректной работы приложения, поскольку если его не указывать, то при редиректе добавленные атрибуты-примитивы (строки, целые или булевы значения) будут добавлены спрингом в URL редиректа, и юзер увидит у себя в адресной строке то, что по идее он видеть не должен. Скорее всего, таким образом обрабатываются все атрибуты модели, которые можно сериализовать. Поэтому будьте внимательны. Ну и не стоит забывать о том, что modelAndView может принимать значение null в случае, если метод-обработчик был помечен аннотацией @ResponseBody.
3