11月小报|读小报,涨知识

本期知识小报的主要内容包括:

  • • Lombok 之 @Builder 注解与 JSON 反序列化的冲突

  • • 如何动态设置日志输出级别

  • • JWT的生成原理和误区

  • • Java 8 parallelStream 避坑指南

Lombok 之 @Builder 注解与 JSON 反序列化的冲突

Lombok 是 Java 开发中常见的工具,可以极大提高开发效率,对冗余代码进行简化。@Builder 是 Lombok 提供的一个快速实现建造者模式的注解。通过该注解,便可以直接进行参数的链式调用构造, 如下所示:

@Builder
@Data
public class ItemData {
    /**
     * 商品id
     */
    public Long itemId;

    /**
     * 商品标题
     */
    public String itemTitle;

}
ItemData itemData = ItemData.builder()
                .itemId(123456L)
                .itemTitle("闲小鱼")
                .build();

一切都看起来非常优雅,直到我们使用 fastjson 进行序列化和反序列化,看似再正常不过的代码,居然抛了如下异常

ItemData itemData = ItemData.builder()
        .itemId(123456L)
        .itemTitle("闲小鱼")
        .build();
//fastjson序列化
String itemString = JSONObject.toJSONString(itemData);
//fastjson反序列化
ItemData deserializeItem = JSON.parseObject(itemString, ItemData.class);
11月小报|读小报,涨知识_第1张图片

通过源码分析得知,fastjson 实例化对象优先使用无参构造函数,其次再使用 public 修饰的带参构造函数。因此反编译看下 @Builder 注解帮我们生成的代码,可以看到只有一个带全部参数的构造函数供 Builder 类内部类使用,并且该构造函数没有被 public 修饰,因此反序列化时 fastjon 无法进行实例化对象。

11月小报|读小报,涨知识_第2张图片

针对上述问题,我们在使用 @Builder 注解时,建议配合 @NoArgsConstructor @AllArgsConstructor 这两个注解使用,可以避免一些情况下的的反序列化问题(jackson , Gson 等序列化框架也会有此问题)。

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ItemData {
    /**
     * 商品id
     */
    public Long itemId;

    /**
     * 商品标题
     */
    public String itemTitle;

}

如何动态设置日志输出级别

日志输出级别定义取决于日志框架,如常见的日志框架Log4j中支持8种日志级别,从低到高依次为OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL;而Logback中支持7种日志级别,从低到高依次为OFF、ERROR、WARN、INFO、DEBUG、TRACE、ALL。日志打印级别由日志配置文件声明,在应用启动时便根据配置文件指定了相关Logger的日志打印级别。那么在应用运行过程中,如何动态变更日志输出级别呢?

对于单台机器,可以使用arthas(阿里巴巴开源的超实用JAVA监控诊断工具)来快速的查看和重置指定Logger的日志打印级别。输入命令logger,查看当前机器上所有Logger的相关信息。

11月小报|读小报,涨知识_第3张图片

输入命令logger --name root --level debug -c 2dde1bff (--name指定Logger名称,--level指定修改到的日志级别) 这里,笔者将主日志日志打印级别调整到了DEBUG。

11月小报|读小报,涨知识_第4张图片

那么对于线上机器集群,如何实现日志级别动态调整呢?答:通过配置中心 + LoggingSystem(org.springframework.boot.logging.LoggingSystem)来实现。LoggingSystem会帮我们屏蔽掉底层具体的日志框架,实现统一的日志操作。这里,笔者为大家提供一个工具类。

@Service
public class LoggerLevelAdjustService {

    @Autowired
    private LoggingSystem loggingSystem;

    /**
     * 设置主日志(ROOT Logger)的日志输出级别
     * @param newLevel 要设置到的日志输出级别
     * @return void
     */
    public void setRootLoggerLevel(String newLevel){
        setLoggerLevel(ROOT_LOGGER_NAME, newLevel);
    }

    /**
     *
     * @param loggerName logger name
     * @param newLevel 要设置到的日志输出级别
     * @return void
     */
    public void setLoggerLevel(String loggerName, String newLevel){
        if (StringUtils.isEmpty(newLevel)) {
            return;
        }
        for (LogLevel level : LogLevel.values()) {
            if (level.name().equals(newLevel)) {
                loggingSystem.setLogLevel(loggerName, level);
            }
        }
    }
}

之后,只需要在配置中心接收配置推送的代码中,调用工具类中修改日志输出级别的方法,即可实现日志输出级别的动态调整。

JWT的生成原理和误区

JWT的全名是JSON Web Token,是一套去中心化的信息传输工业标准,在认证和信息交换领域中被较为广泛地使用。在短时效身份认证场景下,相比于传统的“cookie + session”方案,JWT使用计算替代存储,无需服务端额外存储session,可以减少数据库访问,并具有更好的扩展性。

在结构上JWT由三部分组成:Header,Payload和Signature,并使用“.”将三部分连接形成“Header.Payload.Signature”。在Header中,指明Signature生成过程使用的加密算法,明文如下:

11月小报|读小报,涨知识_第5张图片

在Payload中,放入需要被传输的信息,如:

11月小报|读小报,涨知识_第6张图片

在最终的JWT令牌中,Header和Payload会使用Base64Url算法编码为最终传输信息的前二部分。为了防止信息交换的过程中被篡改,JWT设计了第三部分Signature。Signature通过将Header和Payload的信息进行加密的方式生成,常见支持的加密算法有HS256(一种引入密钥的摘要算法),非对称加密RS256等。若我们采用HS256,则Signature的生成公式为:Signature = HS256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 在设定HS256特定密钥后,我们可以得到Signature为zYAABCrTH4trLliHsSbmCA3Rrn0Fym5aZ5MgrlnkW9Y。最终将编码后的Header、Payload和生成的Signature使用“.”连接起来,便得到了JWT令牌(红色部分为Header,紫色为Payload,蓝色为Signature)。

11月小报|读小报,涨知识_第7张图片

显然,若在传输过程中Payload被进行了篡改,接收方使用预先定义的密钥便会生成截然不同的Signature。因此,JWT可以实现信息传输过程中的防篡改。此外,因为Payload部分仅仅是进行了编码(而非加密),所以在接收方或者第三方可以被解码为明文userId。因此,切记不能使用Payload传输密码等敏感信息。

Java 8 parallelStream 避坑指南

StreamAPI 是 Java 8 中引入的新特性,借助这个能力,开发者们可以更容易地对集合数据进行处理。除了默认的串行 Stream 之外,StreamAPI 还引入了 parallelStream ,只需一行 parallelStream() ,就可以开启多线程并行处理,进而充分发挥多核 CPU 的性能。不过在你使用它之前,最好注意以下几个问题:

  1. 1. 并行流与串行流的执行结果可能会不一致

IntStream.of(1, 2, 3).reduce(4, Integer::sum); 这是一段简单的数组元素相加,得到执行结果为 10,符合预期。 IntStream.of(1, 2, 3).parallel().reduce(4, Integer::sum);
接下来我们将这段代码改写为使用并行流来处理,在其他条件都不变的基础上,得到结果为 18,与预期结果不一致。造成这种差异的原因主要是 Stream 的并行流底层采用的是 ForkJoinPool 来执行,其线程数为运行机器的 cpu 核数-1(因此cpu核数会影响执行结果),其主要原理是把任务分成多个部分,每个部分进行独立计算最后合并。复原一下上面的例子,如下图所示:

11月小报|读小报,涨知识_第8张图片
  1. 2. 串行流并不是在所有场景下都可以带来性能提升

由上面的例子可以看出,并行流更适合处理无状态的数据,即各子任务之间关联性较弱的操作,如 map,filter 等,而 sorted、 distinct 以及 limit 等操作可能并不会带来性能上的提升,反而会因为线程的额外开销导致效率不如并行流。因此,需要视具体使用场景来评估是否使用。

  1. 3. 长耗时任务需要单独指定处理任务的线程池

parallel stream 默认使用的执行线程池为 ForkJoinPool 内部的静态 commonPool,主要用于处理没有为其单独分配线程池的任务。该线程池的大小默认为 cpu 核数-1,因此如果在其中运行长耗时的任务,极有可能造成线程池阻塞,影响当前业务甚至其他用了 parallel stream 的业务。此时更建议手动开启一个新的线程池进行任务处理,或者为 parallel stream 单独指定一个专用线程池,如下所示:

ForkJoinPool businessPool = new ForkJoinPool(4);
int sum = businessPool.submit(
    () -> IntStream.of(1, 2, 3).parallel().reduce(0, Integer::sum)).get();
businessPool.shutdown();

引用

Github fastjson: https://github.com/alibaba/fastjson/wiki/ASMDeserializerFactory%E8%AE%BE%E8%AE%A1

Arthas: https://arthas.aliyun.com/doc/logger.html

JWT: https://jwt.io/introduction

Java8Stream: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html

12cf781a473dc8ce3583e2bd35ec4e95.png

你可能感兴趣的:(java,json,开发语言)