Не так давно из-за требований необходимо было ввести журнал операций. После вызова почти каждого интерфейса в базу данных записывается определенный журнал, связанный с этим параметром. Например, например, при отключении звука, журнал должен записывать причину отключения звука, идентификатор отключения звука и различную информацию. Это удобно для последующих запросов.
Таких интерфейсов много, и большинство из них имеют разные параметры. Идея, которую может легко придумать, состоит в том, чтобы реализовать класс инструмента ведения журнала, а затем добавить строку кода в интерфейс, который должен вести журнал. Этот класс инструментов журнала определяет, какие параметры должны обрабатываться в данный момент.
Но с этим есть большая проблема. Если количество интерфейсов, которые необходимо логировать, очень велико, давайте не будем обсуждать, сколько необходимо оценивать типов в этом классе инструментов, просто добавление такой строки кода ко всем интерфейсам — это, на мой взгляд, неприемлемое поведение. Во-первых, это слишком навязчиво для кода. Во-вторых, если в более поздний период произойдут изменения, обслуживающему персоналу будет очень некомфортно. Представьте, что вы ищете один и тот же код по всему миру и модифицируете его один за другим.
Поэтому я отказался от этого несколько примитивного подхода. Наконец, я принял метод Aop для записи журнала, перехватив запрос. Но даже при таком подходе остается проблема, как поступить с большим количеством параметров. И как соответствовать каждому интерфейсу.
Вместо того, чтобы перехватывать все контроллеры, я настроил аннотацию журнала. Все методы, отмеченные этой аннотацией, будут протоколироваться. В то же время аннотация будет иметь тип для указания конкретного содержимого журнала и параметров для текущего интерфейса.
Итак, как указать соответствующие параметры для текущего журнала из множества возможных параметров. Мое решение состоит в том, чтобы поддерживать класс параметров, в котором перечислены все имена параметров, которые необходимо регистрировать в журнале. Затем при перехвате запроса все параметры и значения в запросе и ответе запроса получаются через отражение.Если параметр существует в классе параметров, который я поддерживаю, ему присваивается соответствующее значение.
Затем после завершения запроса замените все зарезервированные параметры в шаблоне параметрами с присвоенными значениями. Таким образом, требования выполняются, а ремонтопригодность кода также обеспечивается без значительного вмешательства в бизнес.
Ниже я перечислю подробный процесс реализации.
Перед началом операции
В конце статьи я приведу весь исходный код этого демонстрационного проекта. Поэтому, если вы не хотите видеть процесс, Xiongtai может перейти к концу и посмотреть исходный код напрямую. (Я слышал, что читать статью вкуснее, когда она соответствует исходному коду...)
новыйLogAspectДобрый. код показывает, как показано ниже.
package spring.aop.log.demo.api.util;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* LogAspect
*
* @author Lunhao Hu
* @date 2019-01-30 16:21
**/
@Aspect
@Component
public class LogAspect {
/**
* 定义切入点
*/
@Pointcut("@annotation(spring.aop.log.demo.api.util.Log)")
public void operationLog() {
}
/**
* 新增结果返回后触发
*
* @param point
* @param returnValue
*/
@AfterReturning(returning = "returnValue", pointcut = "operationLog() && @annotation(log)")
public void doAfterReturning(JoinPoint point, Object returnValue, Log log) {
System.out.println("test");
}
}
PointcutПередается аннотация, указывающая, что любой метод, помеченный этой аннотацией, вызоветPointcutдекоративныйoperationLogфункция. иAfterReturningОн запускается после возврата запроса.
пользовательская аннотация
Пользовательская аннотация, упомянутая на предыдущем шаге, будет отмечена для каждого метода контроллера. создать новыйannotationтип. код показывает, как показано ниже.
TargetиRetentionВсе являются мета-аннотациями. Всего есть 4 вида, которые@Retention,@Target,@Document,@Inherited.
TargetАннотация указывает область действия, измененную этой аннотацией. Многие типы могут быть переданы, и параметрыElementType. НапримерTYPE, используемый для описания класса, интерфейса или класса перечисления;FIELDиспользуется для описания свойств;METHODиспользуется для описания методов;PARAMETERиспользуется для описания параметров;CONSTRUCTORИспользуется для описания конструкторов;LOCAL_VARIABLEИспользуется для описания локальных переменных;ANNOTATION_TYPEИспользуется для описания аннотаций;PACKAGEИспользуется для описания пакетов и т. д.
RetentionАннотация определяет, как долго сохраняется аннотация. ПараметрыRetentionPolicy. НапримерSOURCEУказывает, что он существует только в исходном коде и не будет существовать в скомпилированном файле класса;CLASSявляется параметром по умолчанию для этой аннотации. Он существует в исходном коде и в скомпилированном файле класса, но не будет загружен в виртуальную машину;RUNTIMEОн существует в исходном коде, файле класса и виртуальной машине, говоря простым языком, его можно получить путем отражения во время выполнения.
добавить обычные аннотации
Добавьте в интерфейс, который должен записывать журналLogаннотация.
После добавления каждый вызовtest/{id}Этот интерфейс вызовет перехватчик вdoAfterReturningкод в методе.
с аннотациями типа
Метод записи обычных журналов описан выше, а метод записи специальных журналов описан далее. Что такое конкретный журнал, то есть информация, которая будет записываться каждым интерфейсом, разная. Для этого нам нужно реализовать класс перечисления типа операции. код показывает, как показано ниже.
Перечисление шаблона типа действия
Создайте новый класс перечисленияType. код показывает, как показано ниже.
После добавления каждый вызов добавляет@Log(type = "WARNING")Этот аннотированный интерфейс будет печатать журнал, указанный этим интерфейсом. Например, приведенный выше код напечатает следующий код.
因被其他玩家举报,警告玩家
Получить параметры запроса, перехваченные aop
Нетрудно указать журнал для каждого интерфейса, просто укажите тип для каждого интерфейса. Но вы также должны были заметить, что журнал интерфейса записывает только因被其他玩家举报,警告玩家Такая информация не имеет никакого смысла.
Человек, который записывал журнал, так не думал, а человек, который, наконец, проверял журнал, каждый день дважды думал о себе. Отчитался за что? Кого я предупреждаю?
Такие журналы выполняют слишком много бесполезной работы, и нет возможности отследить источник после возникновения проблемы. Итак, наш следующий шаг — добавить определенные параметры для каждого интерфейса. Тогда у вас может возникнуть проблема.Если параметры каждого интерфейса почти разные, то этому классу инструмента необходимо передать множество параметров, как его реализовать, и даже организовать параметры, которые будут занимать много бизнес-кода, и будет добавлено много избыточного кода.
Вы можете подумать, реализовать метод для ведения журнала, вызвать его в интерфейсе для ведения журнала и передать параметры. Если типов много, параметры тоже будут увеличиваться, причем параметры каждого интерфейса разные. Это громоздко иметь дело и слишком навязчиво для бизнеса. Код, связанный с логированием, встроен почти везде. После внесения изменений становится очень трудно поддерживать.
Поэтому я напрямую использую рефлексию, чтобы получить все параметры в запросе, перехваченном aop.Если в моем классе параметров есть параметры в запросе (все параметры должны быть записаны), то я пропишу значение параметра в класс параметров. Наконец, замените зарезервированное поле параметра в шаблоне журнала параметром в запросе.
Блок-схема показана ниже.
Новый класс параметров
создать новый классParam, который содержит все параметры, которые могут появиться в журнале операций. почему ты хочешь сделать это? Поскольку параметры, требуемые для каждого интерфейса, могут быть совершенно разными, лучше поддерживать большое количество логики суждения, чем поддерживать большое количество логики суждения.贪心Одна точка, передать все возможные параметры напрямую. Конечно, если есть новые параметры, которые необходимо записать позже, код нужно модифицировать.
Первое, что нужно сделать, это перехватить запросы с пользовательскими аннотациями. Мы можем получить детали запроса, а также все имена параметров и параметры в запросе. Далее мы реализуем приведенный выше кодgetRequestParamметод.
В приведенном выше коде проходим имена параметров, переданные в запросе, а затем реализуемisExistметод, чтобы определить, находится ли этот параметр в нашемParamСуществует ли класс, если он существует, мы вызовем его сноваsetRequestParamValueIntoParamметода, запишите значение параметра, соответствующее имени этого параметра, вParamв экземпляре класса.
Как мы упоминали выше, геттер и сеттер будут добавлены при компиляции, поэтому первая буква имени параметра будет заглавной, поэтому нам нужно реализовать его самостоятельно.setFirstLetterUpperCaseчтобы сделать первую букву имени параметра, который мы передаем, заглавной.
Эта функция использует метод отражения для получения заданного метода параметра, который будетParamСоответствующий параметр в классе устанавливается на входящее значение.
бегать
Запустите проект и запросите метод в контроллере. И передать определенные параметры.
ДолженGETВсего в запрос передано 4 параметра, которыеid,workOrderNumber,userId, name. Как видите, вParamкласс не определенnameэто поле. Это преднамеренное добавление параметра, который не нужно записывать для проверки надежности нашего интерфейса.
После запуска вы можете увидеть информацию, напечатанную на консоли, следующим образом.
Мы хотим записать все параметры, записанные aop вParamэкземпляр класса, и передача неожиданных параметров не приводила к сбою программы. Далее нам нужно только заменить эти параметры зарезервированными полями параметров ранее определенного шаблона.
Подстановочные параметры
существуетdoAfterReturningсерединаgetRequestParamПосле функции добавьте следующий код.
if (!logDetail.isEmpty()) {
// 将模板中的参数全部替换掉
logDetail = this.replaceParam(logDetail);
}
System.out.println(logDetail);
один из нихgetParamметод, аналогичныйsetParam, который также использует метод отражения для получения соответствующего значения через входящий класс и ключ.
Запросите указанный выше URL еще раз, и вы увидите вывод консоли следующим образом.
因 工单号 [3231732] /举报 ID [8] 警告玩家 [748327843]
Как видите, все параметры, которые нам нужно записать, заменены корректно. Параметры, которые не нужно записывать, также не влияют на программу.
Попробуем передать без необязательных параметров, как это будет выглядеть. Измените контроллер следующим образом и измените workOrderNumber на необязательный параметр.
Затем вы можете увидеть, что вывод консоли выглядит следующим образом.
因 工单号 [空] /举报 ID [8] 警告玩家 [748327843]
На нормальную работу программы это не повлияет.
Получить сложный тип параметра
Следующее, о чем нужно рассказать, — как регистрировать сложные типы параметров. На самом деле общая идея остается прежней. Посмотрим, есть ли во входящем классе параметры, которые нужно записать. Если таковые имеются, замените параметры записи по методике записи простых параметров выше.
Стартовый проект. Используйте postman, чтобы инициировать POST-запрос к указанному выше URL-адресу. Внесите его в тело запросаTestDTOпараметры в . После успешного завершения запроса вы увидите вывод консоли, как показано ниже.
Затем вы можете записать вышеуказанный журнал в соответствующее место в соответствии с вашими потребностями.
В этот момент некоторым приятелям может показаться, что все в порядке, все на месте, только восточный ветер должен. Тем не менее, есть еще несколько проблем с этой реализацией.
Например, что делать, если запрос не выполнен? При неудачном запросе вообще не нужно записывать лог операции, но даже при неудачном запросе будет возвращаемое значение, означающее, что лог будет успешно записан. Это создает много проблем при последующем просмотре журнала.
Другой пример, что, если нужный мне параметр находится в возвращаемом значении? Если вы не используете унифицированный сервис, генерирующий уникальные идентификаторы, вы столкнетесь с этой проблемой. Например, мне нужно вставить новый кусок данных в базу данных, мне нужно получить id автоинкремента базы данных, а наш перехват журнала перехватывает только параметры в запросе. Вот этим мы и займемся дальше.
Определить, был ли запрос успешным
выполнитьsuccessфункция, код выглядит следующим образом.