введение
Элегантный дизайн API — это больше, чем просто代码层面的书写规范.Практически невозможно нормально использовать API после завершения разработки.Это больше касается полировки деталей.Например, каждый раз, когда интерфейс выполняется, входные параметры будут многократно проверяться в тесте API.
считать
Как спроектировать решение, чтобы разработчики могли четко визуализировать время обработки интерфейса и корректность входных параметров?
идеи
Первое, что приходит на ум, — это АОП-аспект Spring.Теперь, когда мы пишем API-интерфейсы, мы обычно пишем интерфейсы на уровне управления контроллером.В зависимости от бизнеса они делятся на классы контроллеров, написанные для разных бизнес-пакетов. общая архитектура такая:В соответствии со спецификацией написания этого уровня управления вам нужно только использовать аспект, чтобы найти класс контроллера в каждом бизнес-пакете, отслеживать входные параметры и время выполнения каждого метода в классе и печатать их в журнале журнала для визуализации. каждый из них в консоли Статус интерфейса в реальном времени.
упражняться
пакет гида
<dependency>
<!--spring启动包-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<!--spring aop核心包-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Ядро АОП
Ядром АОП является切点а также通知类型.В сочетании с решениями, которые нам нужно реализовать, мы сосредоточимся на каждом методе класса пакета уровня управления для каждого бизнеса.
Основные виды уведомлений делятся на:
-
前置通知[Before advice]: предварительное уведомление, выполненное перед точкой соединения, не повлияет на выполнение точки соединения, если только здесь не возникнет исключение. -
正常返回通知[After returning advice]: Выполняется после того, как точка соединения завершила свое нормальное выполнение. Если точка соединения выдает исключение, она не будет выполнена. -
异常返回通知[After throwing advice]: выполняется после того, как точка соединения выдает исключение. -
返回通知[After (finally) advice]: выполняется после завершения выполнения точки подключения, независимо от того, завершено ли обычное выполнение или возникло исключение, будет выполнено содержимое возвращенного уведомления. -
环绕通知[Around advice]: окружающий совет окружает точки соединения, например, до и после вызова метода. Это самый мощный тип уведомлений, позволяющий настраивать некоторые действия до и после вызова метода. Совет Surround также отвечает за принятие решения о продолжении обработки точки соединения (вызов метода continue ProceedingJoinPoint) или прерывании выполнения.
Здесь, поскольку нам нужно записать входные параметры и время обработки интерфейса, выбираемBefore 前置通知а такжеAround 环绕通知
определить точку
Первый шаг резки, нам нужно найти точку разреза
новыйRuntimeMethodКласс, украсьте @Aspect @Component, чтобы определить, что это класс записи аспекта, управляемый spring, аннотация @Log4j2 удобна для последующей печати журналов.
@Aspect
@Component
@Log4j2
public class RuntimeMethod {
//定义aopPoint私有方法,用@Pointcut修饰并标识该切面的切点
//以execution(* com.staging.business.*.controller.*.*(..))为例
//execution()是切面的主体
//第一个" * "符号,表示返回值的类型任意
//com.staging.business表示AOP所切的服务的包名,即需要进行横切的业务类
//包名后面的" .. ",表示当前包及子包
//之后的" * ",表示类名,*即所有类
// .*(..) 表示任何方法名,括号内表示参数,两个点表示匹配任何参数类型
@Pointcut("execution(* com.staging.business.*.controller.*.*(..))")
private void aopPoint() {
}
}
На втором этапе аспекта определите передний и задний советы и объявите точку совета как aopPoint().
/**
* 功能描述: 前置通知
*/
@Before("aopPoint()")
public void before(JoinPoint joinPoint) throws Throwable {
//在调用切面管理的接口前会进入这里
}
/**
* 功能描述: 环绕通知
*/
@Around("aopPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//在before通知后会走入这里,直到返回result对象后,客户端才可以拿到回参
Object result = joinPoint.proceed();
return result;
}
Первые два шага реализуют два уведомления, которые необходимо использовать, и кратко объясняют их функцию, а затем вам нужно использовать пакет spring.ServletRequestAttributesобъект, чтобы получитьHttpServletRequestобъект, получить некоторые параметры печати, которые мы хотим.
public void before(JoinPoint joinPoint) throws Throwable {
//在调用切面管理的接口前会进入这里
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
Enumeration<String> e = request.getHeaderNames();
JSONObject headers = new JSONObject();
if (null != e) {
while (e.hasMoreElements()) {
String headerName = e.nextElement();
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
headers.put(headerName, headerValues.nextElement());
}
}
}
//参数依次代表请求方法,请求地址,参数,头参数,调用时间
log.info("-in- {} {} -{}{}",request.getMethod(),request.getRequestURI(),joinPoint.getArgs(),headers.toJSONString()}
}
Время вызова интерфейса также можно легко распечатать в объемном уведомлении.
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long begin=System.currentTimeMillis();
//在before通知后会走入这里,直到返回result对象后,客户端才可以拿到回参
Object result = joinPoint.proceed();
long end= System.currentTimeMillis();
log.info("-out -time:{}ms", end - begin}
return result;
}
При запуске, вызывая интерфейс API, мы будем выводить следующие логи
-in- GET /user/info -id=123 header:{"content-length":"0",......}
-out- -time:91ms
......
Проблем с тестом нет.Конечно,это не окончательный вариант.Если попробовать поставить в тестовой среде,если будет больше звонящих людей,будет очень запутанно,похоже на стиль ниже.
-in- GET /user/info -id=123 header:{"content-length":"0",......}
-in- GET /teacher/info -id=123 header:{"content-length":"0",......}
-out- -time:91ms
-in- GET /user/info -id=321 header:{"content-length":"0",......}
-out- -time:191ms
......
Видно, что проблема возникает при параллельных операциях.При одновременном вызове нескольких интерфейсов лог будет перепутан, а это не тот результат, которого я хочу.Я должен найти способ решить эту проблему.ThreadLocalлокальные переменные потока иTupleОбъекты кортежа решают эту проблему.Далее модифицируйте код.
Определите закрытую переменную ThreadLocal в классе RuntimeMethod.
private ThreadLocal<Tuple6<String, String, Object[], String, Long, String>> threadLocal = new ThreadLocal<>();
Раздел уведомлений о ремоделировании
@Before("aopPoint()")
public void before(JoinPoint joinPoint) throws Throwable {
//打印请求体
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (null != requestAttributes) {
//在loadingThreadLocal用ThreadLocal和Tuple对象存储参数.这样就可以方便的取出接口的必要参数
loadingThreadLocal(requestAttributes, joinPoint.getArgs());
log.info("-in- {} {} -{}",
threadLocal.get().getT1(),
threadLocal.get().getT2(),
threadLocal.get().getT6());
log.info("Method arguments:{} -{}",
threadLocal.get().getT3(),
threadLocal.get().getT6());
log.info("Request header:{} -{}",
threadLocal.get().getT4(),
threadLocal.get().getT6());
}
}
@Around("aopPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 调用目标方法
Object result = joinPoint.proceed();
String requestUrl = threadLocal.get().getT2();
// 注意在out的时候,取出调用的接口名称,这样可以用接口名称去方便过滤,就不用害怕日志错乱的问题了.return回参在生产环境中尽量不要加进去,因为是测试阶段排查问题打的日志所以越详细越好.
log.info("-out- {} return:{} -time:{}ms -{}", requestUrl, JSONObject.toJSONString(result), System.currentTimeMillis() - threadLocal.get().getT5(), threadLocal.get().getT6());
//接口出参处理
return delReturnData(result);
}
private void loadingThreadLocal(ServletRequestAttributes requestAttributes, Object[] args) {
HttpServletRequest request = requestAttributes.getRequest();
Enumeration<String> e = request.getHeaderNames();
JSONObject headers = new JSONObject();
if (null != e) {
while (e.hasMoreElements()) {
String headerName = e.nextElement();
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
headers.put(headerName, headerValues.nextElement());
}
}
}
//此处追加了一个调用链的id,可返回客户端,让客户端在下一次请求中带入这个id,方法统计一个业务闭环.
String businessId = IdUtil.getSnowflake(1, 1).nextIdStr();
//请求方法,请求地址,参数,头参数,调用时间,调用链id
threadLocal.set(Tuples.of(request.getMethod(), request.getRequestURI(), args, headers.toJSONString(), System.currentTimeMillis(), businessId));
}
Взгляните на журнал вызовов интерфейса после использования этого решения.
2021-01-11 20:16:39.565 [http-nio-8080-exec-7] INFO cn.mc.apd[86] - -in- GET /activityArea/getUserPrize -1348604735921459200
2021-01-11 20:16:39.565 [http-nio-8080-exec-7] INFO cn.mc.appod[90] - Method arguments:[1] -1348604735921459200
2021-01-11 20:16:39.566 [http-nio-8080-exec-7] INFO cn.mc.app.tood[93] - Request header:{"content-length":"0","idfa":"00000",x-nondec-sign":"d93207ba","host":"80""} -1348604735921459200
2021-01-11 20:16:39.593 [http-nio-8080-exec-7] INFO cn.mc.app.tools.interceptor.RuntimeMethod[126] - -out- /activityArea/getUserPrize return:{"code":0,"data":{"userActivePrizeRec":"0","message":"成功"} -time:28ms
постскриптум
Пока что упрощенная версия схемы входных параметров интерфейса и схемы статистики продолжительности интерфейса все живы.Всем нужно обратить внимание, что этот метод приведет к слишком большому количеству избыточных журналов и стараться избегать его использования в производственной среде, которые могут быть добавлены в заголовке класса.@Profile({"dev", "test"})Укажите среду.В производственной среде вы можете использовать Ali Dad'sArthasилиzipkin链路追踪решить.切记, это решение удобно только для устранения проблемы некорректных входных параметров и чрезмерного времени работы интерфейса в период тестирования.исходный адрес гитхаба