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 来完成从入口层对业务服务的执行调用”

spring cache 接口层缓存的演进过程

在spring 体系中,使用spring cache并结合redis来进行数据缓存是很常见的做法。不过,针对于具体的业务场景,可能会有不同的处理方法。

像以下的1个业务场景,即有不同的处理方式。

前端访问后端的指定请求路径(GET类请求), 针对特定的条件下(对应cache condition),希望这个结果能够被缓存.同时,支持当资源修改之后,让此缓存失效掉。

此场景的典型方案就是使用spring cache的 @Cacheable 注解 和 @CacheEvict 注解,并结合实际场景进行混合处理。

在这个实际的场景当中,经历了 service层缓存,Controller层缓存,和Filter层缓存三个阶段,最终达到业务的需求,并且在性能上更接近于实际的需要。
本文就里面碰到的一些实际问题以及解决方式进行了简单描述,从复杂的框架层修改到简单的拦截处理,在思考思路上进行一个分享。

前提
本文中使用的序列化为jackson,即使用jackson将对象序列化为 json 字符串,再getBytes为 字节数组.

继续阅读“spring cache 接口层缓存的演进过程”

使用javassist生成对象转换器Converter

在编程当中,作为Converter, spring体系自带的conversionService可以解决大部分基本对象的转换问题,但对于在业务系统中写的domain,vo,po等对象.spring是不能完成相应对象之间的转换的, 当然也可以使用类似BeanUtils.copyProperties来完成属性之间的数据复制功能. 除此之外,还可以使用第三方组件,比如dozer,都可以进行信息复制处理.

不过上面的方法的问题在于, 除扩展之外,相应的转换过程均是使用反射来完成的.比如通过 PropertyDescriptor 来获取property的readMethod, 然后再通过writeMethod来写入目标对象的数据值,或者直接通过Field.set来完成字段级的数据写入.从编码手法上来看,当前我们更希望通过一些非反射的手法来完成这个操作, 一种实现方法即是通过字节码生成来构建一个特定场景的Converter, 直接通过方法调用来完成相应的映射过程.

一个标准的转换器接口定义如下:

public interface Converter<S, T> {
    T convert(S s);
}

实现者除了要完成具体的convert过程之外, 还需要对外暴露出具体的泛型信息,以方便框架进行读取和解析. 比如spring conversionService即会通过读取converter实现类泛型来完成内部 from -> to的映射过程(而不需要调用者手动进行类型传参).

本文描述了一种通过javassist,字节码操作工具, 动态地读取from,to类的描述信息和注解信息, 完成convert body的字符串生成. 同时, 写入相应的泛型信息, 以实现泛型编程.

继续阅读“使用javassist生成对象转换器Converter”

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

本文示例代码: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

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

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