Monthly Archives: April 2014
Предыстория: у меня был проект – веб-приложение, работающее в GlassFish. Приложение собиралось в EAR а слой данных был реализован на базе JPA
. В качестве реализации JPA использовался EclipseLink 2.5
, встроенный в GlassFish 4.0
. База данных – Oracle 11g Express Edition.
Вообще, до этого я всегда работал с Hibernate, и EclipseLink заставил немного помучаться с особенностями восприятия JPQL. Например, EclipseLink не хотел понимать count(*)
и все используемые таблицы обязывал снабжать алиасами. Но это мелочи, всё было хорошо, пока не понадобилось сделать несложный запрос: получить объекты с условием, в котором было необходимо применение арифметики с датами. Итак, краткая история мучений.
JPQL: OPERATOR()
Вычитал, что в EclipseLink JPQL можно использовать оператор OPERATOR('AddDate')
тынц. Как оказалось, в EclipseLink этот оператор реализован не был. Что в принципе удивительно, т.к. вендором EclipseLink сейчас является Oracle, и нормальный человек в этом случае имеет все основания ожидать хорошей поддержки их СУБД.
JPQL: SQL()
Попробовал также и SQL()
, позволяющий вставлять в JPQL вставки нативного SQL тынц. К сожалению, и тут не получилось. Результирующий SQL был с багом, говорящем о несоответствии количества открывающих и закрывающих скобок.
Пробуем @NamedNativeQuery
Отчаявшись решить проблему в рамках JPQL, решил спуститься на ступеньку ниже и заюзать Native Query
. Но запрос мой требовал соединения четырёх сущностей, и некоторые поля в них назывались по умолчанию одинаково (ID
). Аннотация @EntityResult
позволяет для таких ситуаций задать discriminatorColumn
, но мне не удалось заставить его работать. Маппинг работал некорректно, значения пропертей маппились некорректно в любом варианте использования. Более того, выяснилось, что все столбцы должны иметь уникальный префикс. Иначе вложенные объекты могут захватить свойство (например, ID
) внешнего объекта, чей столбец ID
был без префикса. Всё это выглядит настолько бажным, что становится странным, как такое количество багов может считаться приемлемым.
Работающий маппинг теперь имел вид:
@SqlResultSetMapping( name = "request-department", entities = { @EntityResult( entityClass = Request.class, fields = { @FieldResult( name = "id", column = "R_ID"), @FieldResult( name = "deadlineDate", column = "R_DEADLINE_DATE"), @FieldResult( name = "correctedDeadlineDate", column = "R_CORRECTED_DEADLINE_DATE") }), @EntityResult( entityClass = Department.class, fields = { @FieldResult( name = "id", column = "DEP_ID"), @FieldResult( name = "description", column = "DEP_DESCRIPTION"), @FieldResult( name = "name", column = "DEP_NAME") }), @EntityResult( entityClass = User.class, fields = { @FieldResult( name = "id", column = "U_ID"), @FieldResult( name = "email", column = "U_EMAIL"), @FieldResult( name = "personalFirstName", column = "U_FIRST_NAME"), @FieldResult( name = "personalLastName", column = "U_LAST_NAME"), @FieldResult( name = "personalMiddleName", column = "U_MIDDLE_NAME") }), @EntityResult( entityClass = User.class, fields = { @FieldResult( name = "id", column = "ASS_ID"), @FieldResult( name = "email", column = "ASS_EMAIL"), @FieldResult( name = "personalFirstName", column = "ASS_FIRST_NAME"), @FieldResult( name = "personalLastName", column = "ASS_LAST_NAME"), @FieldResult( name = "personalMiddleName", column = "ASS_MIDDLE_NAME") }) }) |
Left join и вложенные объекты
Тут я вспомнил, что мне на самом деле нужен не INNER JOIN
, а LEFT JOIN
. То есть мне нужно сделать один из @EntityResult
– nullable. Но этого сделать нельзя ! EclipseLink видит, что User.Id
– первичный ключ, и падает на утверждении, что он не может быть Null
. Ничего нельзя сделать, кроме того, что подставлять в столбец магическое число, сигнализирующее о том, что на самом деле сущность отсутствует. Вложенные объекты, кстати, тоже подцепить не удаётся. Но можно их зацепить отдельно, а потом вручную присвоить свойствам внешнего объекта.
http://www.eclipse.org/forums/index.php/t/305321/ https://www.java.net/node/675607
Параметры
Ок, после явного прописывания всех столбцов и использования -1 для сигнализации null-объектов, все работало. Теперь я подумал, что неплохо бы добавить в мой Named Native Query параметры. Как оказалось, это невозможно. Параметры в named native queries не работают вообще и никак. Не работает ничего: ни именование с двоеточия, ни вопросиками, ни через решётку (как предлагают в одном из ответов).
http://eclipse.1072660.n5.nabble.com/Params-on-NamedNativeQuery-td3401.html
Пришлось убрать аннотацию @NamedNativeQuery
, и сделать всё руками:
Query nativeQuery = entityManager.createNativeQuery( String.format( sql, daysBeforeDeadline, RequestStatus.Sent.ordinal(), RequestStatus.Processing.ordinal() ), "request-department" ); List<Object[]> resultList = nativeQuery.getResultList(); for ( Object[] rowObjects : resultList ) { Request request = ( Request ) rowObjects[0]; Department department = ( Department ) rowObjects[1]; User user = ( User ) rowObjects[2]; User assignee = (User) rowObjects[3]; request.setDepartment( department ); request.setUser( user ); if (assignee.getId() != -1) request.setAssignee( assignee ); requests.add( request ); } return requests; |
Прикол в том, что и здесь параметры тоже не работают ! Приходится вставлять их с помощью String.format()
.
(Тут вроде у мужиков работает, но у меня и так не заработало)
Заключение
В общем, я рад, что таки-получилось заставить это работать, но какой ценой ? Программистам EclipseLink стоит задуматься о качестве своего продукта, если на такие простые вещи за столько лет разработки у них не отработаны работающие сценарии на одной из самых популярных СУБД, тем более что они теперь – вендоры этой ORM. Но если вы желаете использовать в своём проекте native queries, то лучше придерживаться следующих правил:
- Не использовать @NamedNativeQuery
- Явно задавать все имена столбцы, они должны быть уникальны и не должны совпадать по названию ни с одним из свойств вложенных сущностей. Лучше всего задать уникальные префиксы для каждой сущности.
- Там, где ожидаются nullable-столбцы, но которые мапятся на свойства, не допускающие значений
null
, придётся использовать магические константы или другие столбцы в качестве флагов - Параметры не работают – придётся вставлять их вручную
Часто бывает нужно передать данные с серверной части на клиентскую, причём не в виде текста, а в виде готовых объектов. К сожалению, в HTML-страницу нельзя внедрить данные напрямую, но можно сгенерировать js-скрипт, в который бы передавался JSON, готовый к употреблению. Обычно я делал это вручную, но теперь решил написать спринговый interceptor
, который бы перехватывал все атрибуты model
, начинающиеся с “js_”, и добавлял их на клиент автоматически.
Пример использования
@RequestMapping(value = "/create", method = RequestMethod.GET) public String viewCreate(Model model) { NewsArticle article = new NewsArticle(); // Этот атрибут будет доступен только в JSP при генерации страницы model.addAttribute( "article", article ); // А этот атрибут также попадёт и в js-код в качестве атрибута глобального объекта JS_DATA model.addAttribute( "js_article_id", article.getId() ); return "edit"; } |
$(function() { var articleId = JS_DATA['js_article_id']; // ... }); |
Код
Все необходимые кусочки файлов можно посмотреть на github, а здесь приведу только код интерсептора:
public class JsDataInjectionInterceptor extends HandlerInterceptorAdapter { private final static Log log = LogFactory.getLog( JsDataInjectionInterceptor.class ); @Override public void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView ) throws Exception { // if (null == modelAndView) return; Map<String, Object> jsObjects = null; for ( Map.Entry<String, Object> entry : modelAndView.getModelMap().entrySet() ) { if (entry.getKey().startsWith( "js_" )) { if (null == jsObjects) jsObjects = new HashMap<>( ); jsObjects.put( entry.getKey(), entry.getValue() ); } } if (jsObjects != null) { ObjectMapper mapper = new ObjectMapper(); mapper.enable( SerializationFeature.INDENT_OUTPUT ); String jsData = mapper.writeValueAsString( jsObjects ); log.debug( "JsData: " + jsData ); modelAndView.getModelMap().addAttribute( "__js_data__", jsData ); } } } |
Узнал о приложении Secure Shell – ssh-клиенте и эмуляторе терминала, работающем в броузере. Меня удивило, что эта штука, написанная на js, действительно работает – неужели протокол ssh с шифрованием действительно был реализован на javascript? Или он проксирует данные через внешний сервер, передавая туда сочетания клавиш, а обратно получая команды для эмулятора терминала в plain text ? При этой мысли по спине пробежал холодок. Я только что подключался к одному серверу и ввёл там правильный пароль. Если догадка верна, то мой пароль мог легко попасть к злоумышленникам. В общем, пришлось поковырять поглубже, чтобы выяснить, как всё работает.
После изучения FAQ, исходников и файлов, поставляемых с расширением, выяснено было следующее:
-
Это приложение написано в рамках проекта
Chromium OS
, и его исходники открыты. -
Оно действительно написано на js, но реализации ssh в нём нет, но есть нативный плагин, встроенный в расширение при помощи технологии
Native Client
. В нём-то и лежит реализация ssh-протокола, а доступ к нему осуществляется через джаваскриптовое API, предоставляемое броузеромGoogle Chrome
. -
Но механизм проксирования также присутствует ! Называется он
Relay Server
, и иногда работает. Из документации непонятно, при каких ситуациях используется именно этот режим. Написано что по умолчанию используется native client, но можно и специально заставить использовать relay, указав его опции в строкеRelay options
. Почему-то списка доступных настроек найти не удалось. По исходникам тоже непонятно, в каком случае используется native client, а в каком – relay server. Исходные коды гуглового relay сервера закрыты, в комментариях написано, что “Вы можете написать такой сервер сами, вам достаточно зареверсинжинирить такой-то js-файл”. -
Есть джавский relay-сервер в открытых источниках: https://github.com/zyclonite/nassh-relay/
Демосервер, на нём можно проверить работу https://relay.wsn.at/
Заодно мы узнаём опции, которые нужно добавить в строку relay options:
--proxy-host=relay.wsn.at --proxy-port=443 --use-ssl
Решил проверить этот сервер, не вводя там правильного пароля – всё заработало, но строка Loading Native Client всё равно смущает. Попробовал удалитьssh_client_nl_x86_64.nexe
, после этой операции Secure Shell не хотел подключаться, хотя опции для релея были. -
Вот ещё один relay-server, здесь уже на пейтоне: https://github.com/raptium/hashi
Выглядит очень лаконично, по сравнению с джавской реализацией. Заодно узнаём ещё один способ задания relay-server:
To connect via the relay server, use USER@SSH_SERVER[:SSH_PORT]@RELAY_SERVER as the destination in the secure shell.
-
Native Client – очень интересная фигня. Файлы nexe, поставляемые с расширением, разные для разных процессорных платформ, но одинаковые для операционных систем. То есть
ssh_client_nl_x86_64.nexe
может успешно использоваться и на Windows, и на Linux, и на Mac OS X, главное чтобы система была 64-битная. Это обстоятельство меня смутило, т.к. проводя первичный осмотр в Total Commander, я обнаружил, что эти файлы начинаются с ELF, а значит, что это либо работает только на линуксах, либо где-то дальше в файле лежат нативные файлы в других форматах –PE
,dylib
. Такой типа самопальныйfat executable
. Но всё оказалось ещё интереснее. Выяснилось, что гугл в этом месте не хранит нативный готовый код, а какой-то байткод, получаемый на выходе nacl-toolchain. В сети упоминался байткод LLVM, но тут пока неясно, в чём отличие pexe от nexe. pexe не зависит от платформы, nexe-зависит. Интересно, чем они отличаются внутри. С этим пока разобраться не удалось.
Дополнительный прикол заключаются в том, что эти бинарники безопасны, хотя и содержат нативный код. Дело в том, что они могут быть собраны только с теми библиотеками, которые портированы вnacl-ports
. А верификатор броузера при загрузке этого кода проверяет, не содержит ли он потенциально небезопасных инструкций – к таким, например, относятсяsyscall
иint
.
Общий вывод
Secure Shell
скорее всего безопасен для применения, но точно не ясно, при каких случаях может включаться механизм проксирующих серверов, который небезопасен по своей сути. При paranoid_mode = ON лучше либо не пользоваться этой тулзой, либо до конца разобраться в логике включения релеев.
0