Продолжительность звонка в элегантном интерфейсе печати | семидневный пунш

Java
Продолжительность звонка в элегантном интерфейсе печати | семидневный пунш

введение

Элегантный дизайн 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链路追踪решить.切记, это решение удобно только для устранения проблемы некорректных входных параметров и чрезмерного времени работы интерфейса в период тестирования.исходный адрес гитхаба