Читатель, привет!
Если ты опытный разработчик, то ты это уже давно знаешь и используешь. Если же нет… то самое время узнать, чтобы иметь основания считать себя хорошим разработчиком ) .
Сама идея проста, как колумбово яйцо (или «проста как валенок», с учётом работающего у нас сейчас импортозамещения).
А именно: все вызовы из системы вовне и все вызовы системы извне должны быть обёрнуты минимум одним слоем прокси методов.
Как видите, очень просто.
Но разберём чуть подробнее.
"Зачем это нужно?" или подавляющие преимущества проксирования
-
Во-первых, для безопасности. Допустим нам требуется реализовать такую надуманную и максимально упрощённую задачу как выбор данных из базы, причём имя таблицы мы должны задавать динамически.
Отлично, пишем веб-контроллер, принимающий имя таблицы в виде строки, вставляем это имя в sql-запрос и через jdbc подключение отправляем его базу и возвращаем результат выполнения запроса пользователю. Казалось бы: что может пойти не так? )
Если тебя, читатель, сейчас не передёрнуло от ощущения неминуемого «ахтунга», то немедленно беги читать информацию на тему «sql-инъекций».
Спасти себя от последующего «ахтуга» в данной задаче можно, например, если перед формированием запроса, сначала проверять существование таблицы с полученным от пользователя именем.И, в целом использовать полученные от пользователя данные «как есть» без какой-либо обработки, также, как и отдавать ему данные в «сыром» виде - очень плохая практика. На эту тему можно почитать об использовании DTO-объектов (Data Transfer Object).
Однако, мы отвлеклись от сути.
А суть в том, что если бы каждый наш запрос к базе проходил бы какую-то предобработку, то шанс определить «плохой» запрос не просто возрастает - он, как минимум, в принципе появляется, такой шанс.
-
Во-вторых, единая точка входа это всегда хорошо. Тем, кто хотя бы раз в жизни занимался интеграцией различных систем между собой данный тезис должен казаться аксиомой.
Пожелай мы добавить в проект логирование всех запросов или сбор статистики (хотя бы времени выполнения того или иного запроса в целях определения узких мест системы при нагрузочном тестировании) сделать это будет на порядки проще если все наши исходящие (и входящие) запросы проходят через некий прокси-метод где мы и сможем расположить наш код логирования и сбора статистики.
Мы можем захотеть добавить некую логику, например, анализ входящего/исходящего запроса или простое управление доступом к удалённой системе - всё это будет гораздо сложнее, если в нашем проекте существуют десятки (сотни? тысячи?) отдельных вызовов не проходящих через некий общий прокси-метод.
"В чём подвох?" или недостатки проксирования
Недостаток только один и он заключается в том, что прокированный запрос работает явно медленнее. Сложно назвать это недостатком так как любому разработчику абсолютно ясно, что добавление дополнительной логики к существующему коду влечёт замедление скорости работы этого кода, как минимум, на время потраченное на обработку добавленной логики.
Когда нужно использовать прокси-методы?
Ответ прост: всегда. Абсолютно всегда.
Если у вас есть входящий запрос к вашей системы извне или исходящий от вашей системы запрос во вне - он должен пройти через прокси-метод и, вероятно, не через один.
Вполне удобно иметь в проекте некую иерархию прокси-методов
Вы можете разделить прокси по используемым технологиям, по системам к которым выполняете (от которых получаете) запросы, по типу запросов, по пересылаемым бизнес-данным или по всему перечисленному сразу.
Имея продуманную иерархию вызываемых прокси-методов всегда можно добавить требуемый код пред- или пост- обработки запроса именно туда, куда это действительно нужно.
Меньше слов, больше кода
Прокси-методы для обёртки исходящих вызовов методов работающих с базой данных
Допустим, что все наши методы обращения к БД имеют два параметра
Connection - содержащий jdbc-соединение с нужной базой данных
TablePojo - некий POJO (Plain Old Java Object) объект содержащий в себе необходимые данные для выполнения в базе полезной работы
В таком случае о мы можем использовать следующий прокси выполняющий функции логирования и подсчёта времени, затраченного на выполнение метода работающего с данными в базе.
/**Прокси для запросов в БД использующих два параметра
* @param function - ссылка на метод имеющий два параметра
* @param connection - подключение к БД
* @param tablePojo - объект содержащий необходимые данные для выполнения методом полезной работы
* @param <O> - тип выходного параметра
* @param logMessage - текстовое сообщение для записи в лог
* */
public <O> O proxyDbRequest(String logMessage, Connection connection, TablePojo tablePojo, BiFunction<Connection, TablePojo, O> function){
log.info("start: "+logMessage);
long begin=System.currentTimeMillis();
try{
O output = function.apply(connection, tablePojo);
log.info("end successful: "+logMessage+". Spent="+(System.currentTimeMillis()-begin));
return output;
}catch(Exception e){
log.log(Level.WARNING, "end error: "+logMessage+". Spent="+(System.currentTimeMillis()-begin), e);
throw new RuntimeException(e);
}
}
Вызвать метод через данный прокси можно как
this.proxy.proxyDbRequest("create table " + table.getName() + " if not exists", conTest, table, this::createTableifNotExist)
Где проксируемый метод имеет следующее описание
private Boolean createTableifNotExist(Connection connection, TablePojo table)
Как можно заметить, использование данного прокси-метода жёстко ограничивает нас в количестве и типе используемых параметров так как мы задействовали стандартный интерфейс BiFunction.
А что если нам необходимо использовать три параметра, а не два?
Нет ничего проще, напишем собственный интерфейс
/**Функциональный интерфейс функции от трёх аргументов (как BiFunction, только три аргумента)*/
@FunctionalInterface
public interface TriFunction<I1,I2, I3, I4> {
void apply(I1 i1, I2 i2, I3 i3, I4 i4);
}
Перегрузим наш прокси-метод как
/**Прокси для запросов в БД использующих четыре параметра
* @param function - ссылка на метод имеющий четыре входных параметра и ни одного выходного
* @param i1 - первый входной параметр
* @param i2 - второй входной параметр
* @param i3 - третий входной параметр
* @param i4 - четвёртый входной параметр
* @param logMessage - сообщение для лога
* */
public <I1, I2, I3, I4> void proxyDbRequest(String logMessage, I1 i1, I2 i2, I3 i3, I4 i4, TriFunction<I1, I2, I3, I4> function){
log.info("start: "+logMessage);
long begin=System.currentTimeMillis();
try{
function.apply(i1, i2, i3, i4);
log.info("end successful: "+logMessage+". Spent="+(System.currentTimeMillis()-begin));
}catch(Exception e){
log.log(Level.WARNING, "end error: "+logMessage+". Spent="+(System.currentTimeMillis()-begin), e);
throw new RuntimeException(e);
}
}
И будем его использовать как
this.proxy.proxyDbRequest("table processing " + tablePojo.getName(), conProd, conTest, tablePojo, filePojo.getSettings(), this::tableProcessing);
Где описание проксируемого метода выглядит как
private Boolean tableProcessing(Connection conProd, Connection conTest, TablePojo table, SettingsPojo settings)
Прокси-методы для обёртки методов вызываемых при получении запросов по веб-апи
Код простого прокси-метода
/**
* Прокси для веб-запросов вида: "дто запроса" => "дто ответа"
*
* @param className- имя веб-контроллера
* @param methodName- имя вызваемого метода веб-контроллера
* @param req - дто запроса
* @param function - метод сервиса выполняющий преобразование из дто запроса в дто ответа
* @param <R> - тип дто ответа
* @param <T> - тип дто запроса
* @return - вернёт дто ответа
*/
public <T, R> R proxyWebRequest(String className, String methodName, T req, Function<T, R> function) {
log.logp(Level.INFO, className,methodName,"method start with params: " + (req != null ? req.toString() : "no params"));
try {
R resp = function.apply(req);
log.logp(Level.INFO, className,methodName,"method end with SUCCESS! Results: " + (resp==null ? "null" : resp.toString()));
return resp;
} catch (Exception e) {
log.logp(Level.WARNING, className,methodName,"method end with ERROR!", e);
throw new RuntimeException(e);
}
}
Пример использования
...
@PostMapping(value = "/start", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public СreateNewProcessResponse createNewProcess(СreateNewProcessRequest req) {
return this.proxyWebService.proxyWebRequest(this.getClass().getName(),"createNewProcess" , req, startService::createNewProcess);
}
Заголовок проксируемого метода
public СreateNewProcessResponse createNewProcess(СreateNewProcessRequest req)
Вы никогда не можете знать когда и какую именно логику вы захотите добавить к обработке входящего или исходящего запроса любого типа. Но имея цепочку "обёрток" в виде прокси-методов вы сможете легко добавить нужную логику.
Используемые в статье фразеологизмы
Честно говоря я несколько колебался добавлять ли к статье этот раздел или нет. С чисто профессиональной точки зрения он избыточен и не несёт смысловой нагрузки. Но лично мне было интересно вбить в поиск часто используемое выражение, узнать историю его происхождения и появления в русскоязычном пространстве.
"Колумбово яйцо"
Означает: простой выход из затруднительного положения.
По преданию, когда Колумб во время обеда у кардинала Мендосы рассказывал о том, как он открывал Америку, один из присутствующих сказал: «Что может быть проще, чем открыть новую землю?». В ответ на это Колумб предложил ему простую задачу: как поставить яйцо на стол вертикально? Когда ни один из присутствующих не смог этого сделать, Колумб, взяв яйцо, разбил его с одного конца и поставил на стол, показав, что это действительно было просто. Увидев это, все запротестовали, сказав, что так смогли бы и они. На что Колумб ответил: «Разница в том, господа, что вы могли бы это сделать, а я сделал это на самом деле».
"Ахтунг"
Всего лишь «внимание» в переводе с немецкого языка. В русскоязычном интернете начала 2000-х годов оно стало употребляться в значении «осторожно, геи!». Например: Немецкая книжка «Папин друг» это просто полный ахтунг!
Надеюсь, описывать значение термина "валенок" не требуется ни для кого из читателей статьи? )
Комментарии (8)
nerudo
04.07.2022 10:17+19> Недостаток только один и он заключается в том, что прокированный запрос работает явно медленнее. Сложно назвать это недостатком
Суть современного ПО
SnowBearRu
04.07.2022 11:54+1Раньше всякие нужные \ не нужные логирования и метрики через аспекты делали. Надо - включил , не надо - выключил. Да и проверки не стандартные можно на аспекты положить. Код конечно становится запутанным, но вот проксировать все - кажется тоже не улучшает структуру кода....
gwisp
04.07.2022 13:54Замечу, что зачастую библиотеки для взаимодействия с внешним миром уже содержат средства для 'проксирования'.
JDBC содержит интерфейс Connection, который вы можете обернуть, добавив свою логику.
Если используете JPA, то можно обернуть EntityManager
Если обрабатываете HTTP запросы, то для этих целей есть фильтры
Moraiatw
04.07.2022 14:23+2Спасти себя от последующего «ахтуга» в данной задаче можно, например, если перед формированием запроса, сначала проверять существование таблицы с полученным от пользователя именем.
Проверить видимо тоже SQL запросом, подставив в него полученное от пользователя имя? Отлично)
Throwable
04.07.2022 17:40+5Может я немного отстал от жизни, AOP уже тоже вышел из моды? Я думал в статье коснутся деталей Spring AOP, AspectJ, java.lang.Proxy, ByteBuddy, а статья о том как добавить кучу boilerplate-а к бизнес коду.
moonster
05.07.2022 00:40+2Изолировать бизнес-слой от ввода / вывода нужно в первую очередь для уменьшения зацепления и достижения семантической чистоты.
Это имеет смысл делать в любом проекте, который пилят больше одного человека или который займет больше пары дней.
А всякие там аутентификации, профилирования, журналирования, защита от SQL инъекций и т.д. - это решается либо ситуативно (PreparedStatement), либо фреймворками / аспектами.
urvanov
Там это, ещё не переборщить бы с этим прокси. Особенно для пет-проектов. А то после 30-го AbstractSingletonProxyFactoryBean энтузиазм обычно уже заканчивается, а проект почти не двигается.