基于Servlet3.0异步特性实现请求鉴权与转发

项目背景

在多个内网系统之上,增加一个网关服务,统一对第三方应用进行鉴权与认证,方可对内部资源服务进行访问,网关服务主要起到鉴权认证,请求转发主要借助Servlet3.0的异步特性实现,结合springboot进行开发。

将请求异步化的好处

同步请求会将整个请求链路的发起,解析,响应在一个同步逻辑中进行。

采用异步处化可以将请求中耗时操作交给线程池做异步处理,在高并发场景下,通过调用一个非web服务线程处理耗时逻辑,提高系统并发性。

由于线程池是隔离的,可以对线程池做业务隔离分组,进行请求分级,监控等。

思路

之前有几篇文章介绍了认证和鉴权的实现思路,可参考系统鉴权流程及签名生成规则,公网API安全--OAuth认证,互联网通用架构技术----公网API安全规范。

转发的思路主要希望可以将客户端请求直接转发到业务系统,网关系统对于请求api,通过识别入参的条件进行不同业务系统的路由,请求api不做干扰直接转发。

基于SpringBoot实现

  1. 在@SrpingBootApplication之上增加@EnableAsync注解。
  2. 如果项目中有自定义Filter,需要增加asyncSupported=true,@WebFilter(asyncSupported = true)。
  3. 通过ContextListener对Context进行监听,context初始化时进行线程池创建,context销毁时进行线程池销毁。
/**
 * Description
 *
 * @author Mr. Chun.
 */@WebListenerpublic class AppContextListener implements ServletContextListener {    /**
     * 通过ContextListener进行线程池初始化
     *
     * @param servletContextEvent
     */
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(                100,                200,                50000L,
                TimeUnit.MILLISECONDS,                new ArrayBlockingQueue<Runnable>(100));

        servletContextEvent.getServletContext().setAttribute("executor", executor);
    }    /**
     * 通过ContextListener进行线程池销毁
     * @param servletContextEvent
     */
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        ThreadPoolExecutor executor = (ThreadPoolExecutor) servletContextEvent.getServletContext().getAttribute("executor");
        executor.shutdown();
    }
}
  1. 创建自定义Servlet,增加asyncSupported=true,@WebServlet(urlPatterns = "/qbs/route", asyncSupported = true)
/**
 * Description
 * ...
 * @author Mr. Chun.
 */@WebServlet(urlPatterns = "/qbs/route", asyncSupported = true)
public class AsyncLongRunningServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private static final Logger logger = LoggerFactory.getLogger(AsyncLongRunningServlet.class);    @Autowired
    private RestTemplate restTemplate;    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        logger.info("==== 进入Servlet的时间:" + new Date() + " ====");

        long startTime = System.currentTimeMillis();
        logger.info("AsyncLongRunningServlet Start::Name=" + Thread.currentThread().getName() + "::ID=" + Thread.currentThread().getId());
        req.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);        //在子线程中执行业务调用,并由其负责输出响应,主线程退出
        AsyncContext ctx = req.startAsync();
        ctx.setTimeout(9000);

        ThreadPoolExecutor executor = (ThreadPoolExecutor) req.getServletContext().getAttribute("executor");
        executor.execute(new AsyncRequestProcessor(restTemplate, ctx, req.getMethod(), req.getParameter("api"))); // 任务提交线程池

        long endTime = System.currentTimeMillis();
        logger.info("AsyncLongRunningServlet End::Name=" + Thread.currentThread().getName() + "::ID=" + Thread.currentThread().getId() + "::Time Taken=" + (endTime - startTime) + " ms.");

        logger.info("==== 结束Servlet的时间:" + new Date() + " ====");
    }
}
  1. 将耗时任务交由独立线程进行处理,通过实现Runable的run()方法实现。
/**
 * Description
 * ...
 *
 * @author Mr. Chun.
 */public class AsyncRequestProcessor implements Runnable {    private static final Logger logger = LoggerFactory.getLogger(AsyncRequestProcessor.class);    private String url = "http://localhost:8080/";    private RestTemplate restTemplate;    private AsyncContext ctx = null;    private String requestMethod = "";    public AsyncRequestProcessor(RestTemplate restTemplate, AsyncContext ctx, String requestMethod, String api) {        this.restTemplate = restTemplate;        this.ctx = ctx;        this.requestMethod = requestMethod;
        url = url + api;
    }    public void run() {        try {            // 业务请求转发在这里处理
            // 请求入参
            Map map = ctx.getRequest().getParameterMap();            // 最终请求参数
            MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
            Iterator iterator = map.keySet().iterator();            while (iterator.hasNext()) {                String key = (String) iterator.next();                if (!ResponseBuilder.isSysParam(key)) {
                    paramMap.add(key, map.get(key).toString());
                }
            }            String json = "";            if ("GET".equals(requestMethod)) {
                json = restTemplate.getForObject(url, String.class, paramMap);
            } else if ("POST".equals(requestMethod)) {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
                HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(paramMap, headers);
                ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
                json = response.getBody();
            }
            ResponseBuilder.responseWrite((HttpServletResponse) ctx.getResponse(), json);
            logger.info("==== 业务处理完毕的时间:" + new Date() + " ====");
            ctx.complete(); // 通知容器,异步处理完成
        } catch (Exception e) {
            logger.error("AsyncExecutor e: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

© 著作权归作者所有

本文分享自微信公众号 - 服务端技术杂谈(ITIBB2014)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-12-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏邹立巍的专栏

Linux 的进程间通信:信号量

Linux环境下主要实现的信号量有两种。根据标准的不同,它们跟共享内存类似,一套XSI的信号量,一套POSIX的信号量。下面我们分别使用它们实现一套类似文件锁的...

68900
来自专栏cmazxiaoma的架构师之路

Java开发技术大杂烩(三)之电商项目优化、rabbitmq、Git、OSI、VIM、Intellj IDEA、HTTP、JS、Java

55570
来自专栏JadePeng的技术博客

使用SpringBoot开发REST服务

本文介绍如何基于Spring Boot搭建一个简易的REST服务框架,以及如何通过自定义注解实现Rest服务鉴权 搭建框架 pom.xml 首先,引入相关依赖,...

52150
来自专栏一个会写诗的程序员的博客

《Spring Boot极简教程》第9章 Spring Boot集成Scala混合Java开发参考资料

本章我们使用Spring Boot集成Scala混合Java开发一个Web性能测试平台。

24620
来自专栏互联网开发者交流社区

SpringBoot与Web开发

15540
来自专栏java达人

spring mvc基于编码配置的原理

使用spring mvc的时候需要注册DispatcherServlet,DispatcherServlet是一个前端控制器,主要用来拦截符合要求的外部请求,并...

239100
来自专栏IT笔记

MongoDB从入门到“精通”之整合JavaWeb项目

好了,前两篇扯了这么多。开始的开始,我们也无须了解的更加深入。重点是要整合到项目中去,在实践中发现问题,追踪问题,然后解决问题。 ? 3004.jpg 准备 M...

74950
来自专栏哎_小羊

GitLab 之 PlantUML 的配置及使用

目录 PlantUML介绍 环境、软件准备 PlantUML Server 安装及 GitLab 配置 实例 Demo 时序图 流程图 活动图 状态图 用例图...

858100
来自专栏青蛙要fly的专栏

Android技能树 — 多进程相关小结

这次是讲Android存储路径及IO的基本操作。因为我们在开发的时候会经常这种方便的需求。这篇文章的内容我写的可能很少,都没有细写。别吐槽。o( ̄︶ ̄)o

9110
来自专栏开发技术

从源码来理解slf4j的绑定,以及logback对配置文件的加载

  项目中的日志系统使用的是slf4j + logback。slf4j作为一个简单日志门面,为各种loging APIs(像java.util.logging,...

24540

扫码关注云+社区

领取腾讯云代金券