使用X-Forwarded-*正确处理浏览器端地址信息

在WEB开发中,有一定的场景中需要拿到基于浏览器视角的路径信息。比如在spring hateoas, spring security,以及基于302跳转的各类登陆,认证体系。在这种场景中,服务端拿到的地址信息可能并不是实际浏览器端访问的路径信息,这时候就要考虑地址转换的事务。特别是当前的业务系统必定前端有Nginx,以及https转http类的反向代理等场景。

本文描述了标准的 X-Forwarded-Host, X-Forwarded-Port 和 X-Forwarded-Proto的使用以及在部分框架中使用的 X-Forwarded-Prefix。同时,在业务框架(如Spring)中已经实际应用的示例代码。在具体场景下应当如何来进行正确地使用。

这些头信息Nginx以及浏览器不会自动地添加到请求头中,需要显示地进行配置。

X-Forwarded-Host
一般配置为 $http_host, 表示获取浏览器在请求时的 HOST 头

X-Forwarded-Port
表示浏览器在访问时的实际端口时,如nginx提供http服务,则为80,如果是https,则为 443。如果为标准服务,此项可以不用设置,仅用于非标准端口时才使用

X-Forwarded-Proto
表示浏览器在访问时的实际协议,如nginx提供https转http反向代理时,这里即要配置为 https,表示浏览器是使用https协议来进行访问,但后端可能用http提供服务。如果两边协议相同,则不用设置

X-Forwarded-Prefix
用于当nginx使用前缀代理后端请求时使用。如 浏览器请求的路径为 serviceName/a/b/c, Nginx在Location端通过serviceName匹配到后端服务时,但把前缀 serviceName去掉,实际访问后端为 /a/b/c时,这时即需要设置此值。

继续阅读“使用X-Forwarded-*正确处理浏览器端地址信息”

Jackson中基于上下文拦截属性输出的两种实现方式

需求如下,在一个类中,有一些字段属性,其是否输出并不是由字段上的JsonIgnore来决定,而是根据从上下文(如request)中传递过来的某些参数决定。如下类:

class A {
    @ContextIgnored("field1")
    private String field1;

    @ContextIgnored("field2")
    private String field2;
}

当上下文值为 field1 时,则表示 A 的最终输出json 中没有字段 field1. 当上下文为 field2 时, 则最终输出没有字段 field2. 其它情况则输出所有字段。

本文讨论Jackson中的处理方式,如果使用 Fastjson,则有很多方式处理,这里不表.

本文描述通过利用 JsonIgnore 注解处理方式解决此问题 和 通过 PropertyFilter 两种方式来完成此需求的处理方式。

继续阅读“Jackson中基于上下文拦截属性输出的两种实现方式”

使用 MockFilterChain 来完成从入口层对业务服务的执行调用

之前进行filter层的cache操作时,并不能完成对后端调用的续命操作,即每次的缓存都是被动触发。当存在调用频率很高的请求时,如果能在缓存快过期时主动地触发后端的重新请求,那就能保证前端的请求始终均为命中缓存层,在性能上会有大大地提高。相应的策略如下:

  1. 首次访问时缓存数据,首次调用时间,过期时间
  2. 每次命中缓存时判断是否应该进行缓存续命
  3. 如果续命,则在返回原有缓存数据的同时,使用异步线程模拟正常访问,以强制使用业务的调用以更新缓存

这里面的第3步,并没有直接将旧有缓存进行重新设置ttl,而是再次请求。主要的考虑在于潜在的旧缓存可能不一致的问题,并且因为使用了异步调用,因此对实际上的前端访问是没有任何影响的。

本文的主要思路参考于(原文为nginx+lua,这里同样适用):
https://segmentfault.com/a/1190000003874328

本文的缓存工作点为Filter层,因此对后端的访问也同样起始于此点,可以理解为从此处继续后续的流程。但如果直接调用 FilterChain.doFilter,则会因为之前的请求已经结束了,此调用将直接报 类似 NPE这种错误。即一个 filterChain 不能即返回前端数据,同时又在新线程时继续原来的处理逻辑。

这里使用了spring-test 中的 mockFilterChain来完成相应的处理。整个思路来源于 AutoConfigureMockMvc 中创建起mockMvc的处理过程。
本文只考虑 GET 请求的处理.

继续阅读“使用 MockFilterChain 来完成从入口层对业务服务的执行调用”

使用统一转换服务来处理不同数据展现的思路和实现

本文示例代码:https://github.com/flym/train-propertytranslate

本文描述了这样一个场景:
针对于一个功能场景,第三方过来的数据格式均不相同,但需要通过一个统一的功能接口来进行调用,然后根据不同来源数据格式进行不同的数据展现。在后端实现时,尽量不要通过if else进行硬编码,而是通过配置的方式来完成数据的处理和呈现。

场景中提到了几个概念,如下:

  • 功能场景和数据来源:分组信息,针对同一个来源,其格式是相同的。不同来源的数据格式不一样
  • 统一功能接口:调用入口是统一的,即方法名,参数定义,以及返回格式都是相同的,仅参数内容不相同
  • 配置化:场景是通用的,可以通过配置来实现,而不是在业务代码中硬编码
  • 不同的数据展现:可以针对不同的分组和场景通过模板来进行渲染,而模板本身是可以配置的,这样即隔离了数据封装这一层。

举例如下, 如下的一个数据内容:

{
    "trade_fullinfo_get_response": {
        "trade": {
            "seller_nick": "我在测试",
            "pic_name": "T1jVXXXePbXXaoPB6a_091917.jpg",

            "receiver_name": "东方不败",
            "buyer_message": "要送的礼物的,不要忘记的哦",
            "receiver_city": "杭州市",
            "receiver_district": "西湖区",
            "orders": {
                "order": [
                    {
                        "title": "苹果",
                        "unit": "个",
                        "oid": 1,
                        "price": 1.1,
                        "sum": 1
                    }
                ]
            },
        }
    }
}

通过转换服务处理,可以展现为下面两种不同的数据内容(以html渲染为例)

渲染场景1
渲染场景2

本文从数据格式,转换器,转换过程,数据渲染几个方面来描述这一思路.

继续阅读“使用统一转换服务来处理不同数据展现的思路和实现”

Jetty中如何实现servlet的异步Request调用

在servlet3.1规范当中,已经支持在servlet中通过调用request.startAsync来开启1个异步调用,然后在相应的业务线程里面进行一些业务操作,再通过asyncContext.complete即完成业务的整个操作。一个参考的demo如下所示:

    val context = req.startAsync();
    //重新设置业务超时时间
    context.setTimeout(40_000);

    Runnable runnable = () -> {
        try{
            //执行你的业务操作

            //输出数据
            context.getResponse().getWriter().println(totalMoney);

            //完成业务
            context.complete();
        } catch(Exception e) {
            e.printStackTrace();
        }
    };
    
    new Thread(runnable).start();

在上面的参考中,原来的servlet在调用完 thread.start之后,相应的逻辑即完成。相应的容器线程则已经还给线程池,此线程即可以接收其它客户端的请求并处理了. 异步servlet的目的即在于将接收请求的io线程与实际的业务执行相分开,避免过慢的业务阻塞了整个容器,而不能再接收更多的请求了.甚至可以在后端的业务池中定义一个队列,将要进行执行的业务逻辑放入队列里面慢慢执行。

由于异步Servlet的目的在于将web容器的io线程与业务线程分离,那么关键的部分即在于当servlet方法执行完之后,当context.complete时,如何重新触发相应io流的操作。本文尝试以jetty为参考,从源码角度查看其实现的原理和机制。

本文参考的jetty版本为 9.4.11.v20180605

继续阅读“Jetty中如何实现servlet的异步Request调用”

如何正确地获取一个有效的数据库连接

这几天在研究各个数据库连接池,比如ali druid, dbcp2 以及最近很火的连接池 HikariCP, 除了常规的池化连接对象管理外。关键区别就是如何创建连接,防止连接泄漏,如何获取连接这些细节的区分点.在hikariCP的wiki中,提到一篇文章 https://github.com/brettwooldridge/HikariCP/wiki/Bad-Behavior:-Handling-Database-Down, 这里面提到由于网络的问题导致获取连接会比预期的时间长的这一问题。这也是我们为什么在选择一些组件和框架时,均会优先使用偏新的版本的原因。一些老的,旧的连接池,因为api的限制,对一些极端情况的处理并不能如意。比如在测试中的c3p0,dbcp这些常规连接池,由于历史原因,在处理一些新的需求时都不能满足需求。

我们来看对于一个标准的数据库连接,我们会有以下的要求:

  1. 在规定的时间内拿到数据库连接
  2. 使用一个alive的connection进行数据访问

整个要求其实就是判断connection存活,处理超时的问题。在常规的数据访问中,因为不同sql查询的原因,我们不能够期望数据库查询在一个较小的时间内就一定能返回(比如5秒)。但是在程序中,当获取一个连接时,又期望在较小的时间内返回给应用,以方便应用能够快速响应业务。如果5s之内(或者更小)不能拿到连接,能认为当前业务失败,避免业务长时间不能响应。甚至避免整个系统所有线程均blocking在获取数据库连接这一步,而导致系统不可用.

那么整个问题变为了以下3步:

  1. 业务在较小的时间内拿到连接,如果超时则立刻返回
  2. 连接池在较小的时间内创建数据库连接,如果超时则立即返回,进行下一步尝试
  3. 连接池在较小的时间内验证已经创建好的连接当前可用,如果超时则立即返回,并标记当前连接不可用,选择其它的连接

继续阅读“如何正确地获取一个有效的数据库连接”