Три способа реализации перехвата в springboot и размышления об асинхронном выполнении

Spring Boot Java задняя часть

способ перехвата спрингбута

В реальных проектах нам часто нужно выводить параметры запроса, результаты ответа, трудоемкость метода, унифицированную проверку разрешений и т. д.

В этой статье сначала представлены три распространенные реализации перехвата в HTTP-запросах, и сравниваются различия.

(1) Метод перехвата на основе аспекта

(2) Метод перехвата на основе HandlerInterceptor

(3) Метод перехвата на основе ResponseBodyAdvice

Рекомендуемое чтение:

Единая структура ведения журналов:GitHub.com/вторая половина/авто-…

MVC

Дело о начале работы с Springboot

Чтобы облегчить всем изучение, давайте начнем с самого простого примера Springboot.

импорт maven

Импортируйте необходимый пакет jar.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.9.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.8.10</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.10</version>
    </dependency>
</dependencies>
<!-- Package as an executable jar -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

стартовый класс

Реализуйте простейший класс запуска.

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

Определить контроллер

Для удобства демонстрации сначала реализуем простой контроллер.

@RestController
public class IndexController {

    @RequestMapping("/index")
    public AsyncResp index() {
        AsyncResp asyncResp = new AsyncResp();
        asyncResp.setResult("ok");
        asyncResp.setRespCode("00");
        asyncResp.setRespDesc("成功");

        System.out.println("IndexController#index:" + asyncResp);
        return asyncResp;
    }

}

где AsyncResp определяется следующим образом:

public class AsyncResp {

    private String respCode;

    private String respDesc;

    private String result;


    // getter & setter & toString()
}

Определение метода перехвата

Аспект на основе

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 *
 * @author binbin.hou
 * @since 1.0.0
 */
@Aspect
@Component
@EnableAspectJAutoProxy
public class AspectLogInterceptor {

    /**
     * 日志实例
     * @since 1.0.0
     */
    private static final Logger LOG = LoggerFactory.getLogger(AspectLogInterceptor.class);

    /**
     * 拦截 controller 下所有的 public方法
     */
    @Pointcut("execution(public * com.github.houbb.springboot.learn.aspect.controller..*(..))")
    public void pointCut() {
        //
    }

    /**
     * 拦截处理
     *
     * @param point point 信息
     * @return result
     * @throws Throwable if any
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        try {
            //1. 设置 MDC

            // 获取当前拦截的方法签名
            String signatureShortStr = point.getSignature().toShortString();
            //2. 打印入参信息
            Object[] args = point.getArgs();
            LOG.info("{} 参数: {}", signatureShortStr, Arrays.toString(args));

            //3. 打印结果
            Object result = point.proceed();
            LOG.info("{} 结果: {}", signatureShortStr, result);
            return result;
        } finally {
            // 移除 mdc
        }
    }

}

Преимущество этой реализации в том, что она является более общей и может быть объединена с аннотациями для достижения более гибких и мощных функций.

Это способ, который мне очень нравится.

Главная цель:

(1) Параметры выхода/входа

(2) Единая настройка TraceId

(3) Отнимающая много времени статистика вызовов методов

На основе HandlerInterceptor

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.DispatcherType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
@Component
public class LogHandlerInterceptor implements HandlerInterceptor {

    private Logger logger = LoggerFactory.getLogger(LogHandlerInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 统一的权限校验、路由等
        logger.info("LogHandlerInterceptor#preHandle 请求地址:{}", request.getRequestURI());

        if (request.getDispatcherType().equals(DispatcherType.ASYNC)) {
            return true;
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.info("LogHandlerInterceptor#postHandle 调用");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

}

Затем вам нужно указать связь между соответствующим URL-адресом и методом перехвата, чтобы он вступил в силу:

import com.github.houbb.springboot.learn.aspect.aspect.LogHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * spring mvc 配置
 * @since 1.0.0
 */
@Configuration
public class SpringMvcConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private LogHandlerInterceptor logHandlerInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logHandlerInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/version");
        super.addInterceptors(registry);
    }

}

Преимущество этого метода в том, что различные методы перехвата могут быть гибко указаны в соответствии с URL-адресом.

Недостатком является то, что он в основном используется на уровне контроллера.

На основе ResponseBodyAdvice

Этот интерфейс имеет метод beforeBodyWrite, тело параметра — это тело ответа в ответе объекта ответа, затем мы можем использовать этот метод для выполнения некоторых унифицированных операций с телом ответа.

Например, шифрование, подпись и т. д.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    /**
     * 日志实例
     * @since 1.0.0
     */
    private static final Logger LOG = LoggerFactory.getLogger(MyResponseBodyAdvice.class);

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        //这个地方如果返回false, 不会执行 beforeBodyWrite 方法
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object resp, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        String uri = serverHttpRequest.getURI().getPath();
        LOG.info("MyResponseBodyAdvice#beforeBodyWrite 请求地址:{}", uri);

        ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) serverHttpRequest;
        HttpServletRequest servletRequest = servletServerHttpRequest.getServletRequest();

        // 可以做统一的拦截方式处理

        // 可以对结果做动态修改等
        LOG.info("MyResponseBodyAdvice#beforeBodyWrite 响应结果:{}", resp);
        return resp;
    }

}

контрольная работа

Запускаем приложение и страницы посещает:

http://localhost:18080/index

Ответ страницы:

{"respCode":"00","respDesc":"成功","result":"ok"}

Журнал серверной части:

c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#preHandle 请求地址:/index
c.g.h.s.l.a.aspect.AspectLogInterceptor  : IndexController.index() 参数: []
IndexController#index:AsyncResp{respCode='00', respDesc='成功', result='ok'}
c.g.h.s.l.a.aspect.AspectLogInterceptor  : IndexController.index() 结果: AsyncResp{respCode='00', respDesc='成功', result='ok'}
c.g.h.s.l.a.aspect.MyResponseBodyAdvice  : MyResponseBodyAdvice#beforeBodyWrite 请求地址:/index
c.g.h.s.l.a.aspect.MyResponseBodyAdvice  : MyResponseBodyAdvice#beforeBodyWrite 响应结果:AsyncResp{respCode='00', respDesc='成功', result='ok'}
c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#postHandle 调用

Последовательность выполнения здесь также относительно ясна и не будет здесь повторяться.

Асинхронное выполнение

Конечно, если это только вышеперечисленное, то это не тема данной статьи.

Далее давайте посмотрим, что произойдет, если ввести асинхронное выполнение.

Определить пул асинхронных потоков

Определить пул асинхронных потоков в springboot очень просто.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * 请求异步处理配置
 *
 * @author binbin.hou
 */
@Configuration
@EnableAsync
public class SpringAsyncConfig {

    @Bean(name = "asyncPoolTaskExecutor")
    public AsyncTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(10);
        executor.setCorePoolSize(10);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }

}

Асинхронно исполняемый контроллер

@RestController
public class MyAsyncController extends BaseAsyncController<String> {

    @Override
    protected String process(HttpServletRequest request) {
        return "ok";
    }

    @RequestMapping("/async")
    public AsyncResp hello(HttpServletRequest request) {
        AsyncResp resp = super.execute(request);

        System.out.println("Controller#async 结果:" + resp);
        return resp;
    }

}

Реализация BaseAsyncController выглядит следующим образом:

@RestController
public abstract class BaseAsyncController<T> {

    protected abstract T process(HttpServletRequest request);

    @Autowired
    private AsyncTaskExecutor taskExecutor;

    protected AsyncResp execute(HttpServletRequest request) {
        // 异步响应结果
        AsyncResp resp = new AsyncResp();
        try {
            taskExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        T result = process(request);

                        resp.setRespCode("00");
                        resp.setRespDesc("成功");
                        resp.setResult(result.toString());

                    } catch (Exception exception) {
                        resp.setRespCode("98");
                        resp.setRespDesc("任务异常");
                    }
                }
            });
        } catch (TaskRejectedException e) {
            resp.setRespCode("99");
            resp.setRespDesc("任务拒绝");
        }

        return resp;
    }

}

Реализация execute также относительно проста:

(1) Основной поток создает AsyncResp для возврата.

(2) Пул потоков асинхронно выполняет конкретный метод подкласса и устанавливает соответствующее значение.

считать

Далее задайте всем вопрос.

если мы попросимhttp://localhost:18080/async,Так:

(1) Каково возвращаемое значение, полученное страницей?

(2) Каково возвращаемое значение вывода журнала аспектов?

(3) Каково возвращаемое значение вывода журнала ResponseBodyAdvice?

Здесь вы можете сделать паузу и записать свой ответ.

контрольная работа

запрос нашей страницыhttp://localhost:18080/async.

Страница отвечает следующим образом:

{"respCode":"00","respDesc":"成功","result":"ok"}

Журналы серверной части:

c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#preHandle 请求地址:/async
c.g.h.s.l.a.aspect.AspectLogInterceptor  : MyAsyncController.hello(..) 参数: [org.apache.catalina.connector.RequestFacade@7e931750]
Controller#async 结果:AsyncResp{respCode='null', respDesc='null', result='null'}
c.g.h.s.l.a.aspect.AspectLogInterceptor  : MyAsyncController.hello(..) 结果: AsyncResp{respCode='null', respDesc='null', result='null'}
c.g.h.s.l.a.aspect.MyResponseBodyAdvice  : MyResponseBodyAdvice#beforeBodyWrite 请求地址:/async
c.g.h.s.l.a.aspect.MyResponseBodyAdvice  : MyResponseBodyAdvice#beforeBodyWrite 响应结果:AsyncResp{respCode='00', respDesc='成功', result='ok'}
c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#postHandle 调用

В сравнении вы можете найти ответы на наши вопросы выше:

(1) Каково возвращаемое значение, полученное страницей?

{"respCode":"00","respDesc":"成功","result":"ok"}

Результат завершения асинхронного выполнения может быть получен.

(2) Каково возвращаемое значение вывода журнала аспектов?

AsyncResp{respCode='null', respDesc='null', result='null'}

Не удалось получить асинхронный результат.

(3) Каково возвращаемое значение вывода журнала ResponseBodyAdvice?

AsyncResp{respCode='00', respDesc='成功', result='ok'}

Результат завершения асинхронного выполнения может быть получен.

Это кажется немного странным, в чем суть причины? Как это проверить?

Асинхронное выполнение

причина

По сути, асинхронное выполнение имеет мало общего с самим механизмом пружины.

Просто сам метод асинхронного выполнения требует времени, чем позже метод перехвата, если асинхронное выполнение завершено, то соответствующую информацию можно просто получить.

Способы идентификации

Как проверить это предположение?

Мы можем добавить сон к процессу.

корректировка кода

  • BaseAsyncController.java

Добавьте некоторую информацию журнала выполнения для выполнения, чтобы сократить время просмотра.

taskExecutor.execute(new Runnable() {
    @Override
    public void run() {
        try {
            logger.info("AsyncResp#execute 异步开始执行。");
            T result = process(request);
            resp.setRespCode("00");
            resp.setRespDesc("成功");
            resp.setResult(result.toString());
            logger.info("AsyncResp#execute 异步完成执行。");
        } catch (Exception exception) {
            resp.setRespCode("98");
            resp.setRespDesc("任务异常");
        }
    }
});
  • MyAsyncController.java

Добавьте время сна при выполнении.

@Override
protected String process(HttpServletRequest request) {
    try {
        TimeUnit.SECONDS.sleep(5);
        return "ok";
    } catch (InterruptedException e) {
        return "error";
    }
}

контрольная работа

доступ к страницеhttp://localhost:18080/async

Страница возвращается следующим образом:

{"respCode":null,"respDesc":null,"result":null}

Соответствующий журнал выглядит следующим образом:

2021-07-10 09:16:08.661  INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#preHandle 请求地址:/async
2021-07-10 09:16:08.685  INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.aspect.AspectLogInterceptor  : MyAsyncController.hello(..) 参数: [org.apache.catalina.connector.RequestFacade@1d491e0]
Controller#async 结果:AsyncResp{respCode='null', respDesc='null', result='null'}
2021-07-10 09:16:08.722  INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.aspect.AspectLogInterceptor  : MyAsyncController.hello(..) 结果: AsyncResp{respCode='null', respDesc='null', result='null'}
2021-07-10 09:16:08.722  INFO 11008 --- [lTaskExecutor-1] c.g.h.s.l.a.c.BaseAsyncController        : AsyncResp#execute 异步开始执行。
2021-07-10 09:16:08.777  INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.aspect.MyResponseBodyAdvice  : MyResponseBodyAdvice#beforeBodyWrite 请求地址:/async
2021-07-10 09:16:08.777  INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.aspect.MyResponseBodyAdvice  : MyResponseBodyAdvice#beforeBodyWrite 响应结果:AsyncResp{respCode='null', respDesc='null', result='null'}
2021-07-10 09:16:08.797  INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#postHandle 调用
2021-07-10 09:16:13.729  INFO 11008 --- [lTaskExecutor-1] c.g.h.s.l.a.c.BaseAsyncController        : AsyncResp#execute 异步完成执行。

Можно обнаружить, что сама пружина по-прежнему выполняется в соответствии с обычным процессом.Поскольку время выполнения процесса слишком велико, три метода перехвата не могут получить асинхронный контент.

отражение

Пишу сюда, моего урожая еще много.

(1) Имя перехватчика

Я обычно называю его перехватчиком логов или чем-то подобным, поэтому в начале в заголовке использовалось три типа перехватчиков, правда, строго говоря, их нельзя спутать.

В противном случае, как упоминалось в области комментариев, фильтр также можно назвать перехватчиком.

Поэтому перехватчик равномерно пересматривается на метод перехвата.

(2) Проблема понимания знания

Когда это было впервые реализовано, из-за того, что время обработки было слишком коротким, люди неправильно поняли, что у Spring будет специальный механизм обработки.

Само исследование еще более тщательное, поэтому эта статья была снова переработана.

резюме

Я надеюсь, что эта статья будет полезна для вас, если у вас есть другие идеи, вы также можете поделиться ими с вами в области комментариев.

Лайки, подборки и переадресация гиков — самая большая мотивация для Лао Ма продолжать писать!

Я старая лошадь, и я с нетерпением жду встречи с вами в следующий раз.

640.png