Читатель, привет!
Если ты опытный разработчик, то ты это уже давно знаешь и используешь. Если же нет… то самое время узнать, чтобы иметь основания считать себя хорошим разработчиком ) .
Сама идея проста, как колумбово яйцо (или «проста как валенок», с учётом работающего у нас сейчас импортозамещения).
А именно: все вызовы из системы вовне и все вызовы системы извне должны быть обёрнуты минимум одним слоем прокси методов.
Как видите, очень просто.
Но разберём чуть подробнее.
"Зачем это нужно?" или подавляющие преимущества проксирования
Во-первых, для безопасности. Допустим нам требуется реализовать такую надуманную и максимально упрощённую задачу как выбор данных из базы, причём имя таблицы мы должны задавать динамически.
Отлично, пишем веб-контроллер, принимающий имя таблицы в виде строки, вставляем это имя в 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-х годов оно стало употребляться в значении «осторожно, геи!». Например: Немецкая книжка «Папин друг» это просто полный ахтунг!
Надеюсь, описывать значение термина "валенок" не требуется ни для кого из читателей статьи? )