Pull to refresh

Проксируйте всё

Reading time6 min
Views13K

Читатель, привет!

Если ты опытный разработчик, то ты это уже давно знаешь и используешь. Если же нет… то самое время узнать, чтобы иметь основания считать себя хорошим разработчиком ) .

Сама идея проста, как колумбово яйцо (или «проста как валенок», с учётом работающего у нас сейчас импортозамещения).

А именно: все вызовы из системы вовне и все вызовы системы извне должны быть обёрнуты минимум одним слоем прокси методов.

Как видите, очень просто.

Но разберём чуть подробнее.

"Зачем это нужно?" или подавляющие преимущества проксирования
  • Во-первых, для безопасности. Допустим нам требуется реализовать такую надуманную и максимально упрощённую задачу как выбор данных из базы, причём имя таблицы мы должны задавать динамически.

    Отлично, пишем веб-контроллер, принимающий имя таблицы в виде строки, вставляем это имя в 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-х годов оно стало употребляться в значении «осторожно, геи!». Например: Немецкая книжка «Папин друг» это просто полный ахтунг!

Надеюсь, описывать значение термина "валенок" не требуется ни для кого из читателей статьи? )

Tags:
Hubs:
Total votes 15: ↑8 and ↓7+1
Comments9

Articles