这是我参与更文挑战的第5天,活动详情查看: 更文挑战
本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接
我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的JavaLib」第一时间阅读最新文章,回复【资料】,即可获得我精心整理的技术资料,电子书籍,一线大厂面试资料和优秀简历模板。
背景
最近,小王很苦恼,他说有一个项目的首页数据加载很慢,他说数据库也加了索引,而且也使用 Redis 对热点数据做了缓存,但是不管怎么优化,数据加载速度还是提升不了。
然后我检查了小王的代码,看到如下一行代码,我说你写这样的代码,你是想被辞退吗?
public List getGoodsList() {
// 省略一些代码
List goods = Optional.ofNullable(getFromCache()).orElse(getFromDB());
// 省略一些代码
return goods;
}
复制代码
问题分析
首先,Optional 这个工具类是一个容器对象,可包含亦可不包含一个非空值。它是我们开发中可作为一种判空的优雅解决方法。对于下面这行代码,意思是如果 x 的值不为 null,则返回 x,否则返回 y。
Optional.ofNullable(x).orElse(y);
复制代码
那按这样的逻辑,小王写的代码那不是应该没问题吗?如果你这样想,那我们运行如下代码看结果。
package com.chenpi;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* @Description
* @Author Mr.nobody
* @Date 2021/06/14
* @Version 1.0
*/
public class Demo {
public static void main(String[] args) {
List goodsList = new Demo().getGoodsList();
}
public List getGoodsList() {
// 省略一些代码
List goods = Optional.ofNullable(getFromCache()).orElse(getFromDB());
// 省略一些代码
return goods;
}
private List getFromCache() {
System.out.println("getFromCache");
List list = new ArrayList<>();
list.add(new Goods("手机"));
list.add(new Goods("电脑"));
return list;
}
private List getFromDB() {
System.out.println("getFromDB");
List list = new ArrayList<>();
list.add(new Goods("手机"));
list.add(new Goods("电脑"));
return list;
}
}
复制代码
运行结果
getFromCache
getFromDB
复制代码
结果发现,即使从缓存已经获取到非空的数据了,但是还是执行了数据库查询操作。所以相当于 Redis 缓存和持久层数据库都进行了查询操作,而且如果从持久层数查询到的数据还要进行费时的计算处理才是最终结果,那可想,即使你怎么优化速度还是提升不了。
打开源码,orElse 方法参数是传入一个值,注意这个值是已经计算好的了,然后再判断 Optional 容器内的对象是否为空,再决定返回哪个对象值。
public T orElse(T other) {
return value != null ? value : other;
}
复制代码
其实,我们应该使用另外一个方法 orElseGet
,就不会出现这样的问题了,源码如下:
public T orElseGet(Supplier extends T> other) {
return value != null ? value : other.get();
}
复制代码
这里使用到了 Supplier
这个函数接口,里面只有一个无参方法,这个方法的作用就是返回一个值。源码如下:
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
复制代码
这就是 JDK8 的函数式编程了,你可以理解为我们可以把一个函数当作一个参数去传递。从 orElseGet 方法看出,只有 Optional 容器内的对象为空的情况下,才会去调用 Supplier 对象的 get 方法。
所以对于小王的代码,应该这样处理,就不会出现缓存有数据还去查询持久层数据的情况了。达到了延迟加载的效果。
List goods = Optional.ofNullable(getFromCache()).orElseGet(this::getFromDB);
复制代码
运行结果
getFromCache
复制代码
其他案例分析
其实 Supplier 这个函数式接口如果用得巧,那优点还是挺多的。例如我们可以发现 JDK 自带的日志 Logger 类中有某些方法就使用到了 Supplier 接口。这样设计有什么好处呢?后面分析细讲。
public void log(Level level, Supplier msgSupplier) {
if (!isLoggable(level)) {
return;
}
LogRecord lr = new LogRecord(level, msgSupplier.get());
doLog(lr);
}
复制代码
我们实际项目中应该是很少使用输出 debug 调试日志的,但是如果某天出现了 bug,我们想输出必要的日志信息用于排除问题,所以我们在程序中可能会编写这样的 debug 调试日志:
log.debug("打印调试日志:{}", JSON.toJSONString(object));
复制代码
但是在生产环境中,为了性能考虑,一般只开启 info 级别,过滤掉 debug 级别的日志。所以上述的 debug 调试日志就不会被输出,但是如果你程序充斥着大量这样的代码的话,有可能你的程序性能会下降。
我们做一个试验,程序只开启 info 日志级别,然后运行程序,看如下的代码的运行结果如何:
public class Demo {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setName("手机");
log.debug("打印调试日志:{}", goods.toString());
}
}
class Goods {
private String name;
@Override
public String toString() {
System.out.println("toString 方法被调用了...");
return "Goods{" + "name='" + name + '\'' + '}';
}
}
复制代码
运行结果
toString 方法被调用了...
复制代码
可能有人会说这没问题,debug 调试信息确实没有打印出来了。但是你是否发现 toString 方法被调用了,如果不是一个简单的 toString 方法,而是一个非常耗时的执行方法,那情况就不一样了。我之前看到有人将一个大量数据的数组打印出来,满屏满屏的调试数据,很可怕。
下面我们演示程序只开启 info 日志级别,但是用 debug 级别的日志调试语句打印一个10000容量的数组数据:
public class Demo {
public static void main(String[] args) {
int size = 10000;
List goodsList = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Goods goods = new Goods();
goods.setName("手机" + i);
goodsList.add(goods);
}
long startTime = System.currentTimeMillis();
log.debug("打印调试日志:{}", JSON.toJSONString(goodsList));
System.out.println("spend time:" + (System.currentTimeMillis() - startTime));
}
}
class Goods {
private String name;
}
复制代码
运行结果如下,因为是开启 info 级别,不会打印 debug 级别的日志,但是这行语句却消耗了353毫秒。如果数组中对象的属性不是一个,而是几十个的话,那消耗的时间就更多了。
spend time:353
复制代码
显然,不管是否开启 debug 级别, JSON.toJSONString(goodsList) 这个方法都被执行了。显然是不合理的,很容易造成性能问题。有一种解决方法,就是先判断是否开启了 debug 级别,这也是很多框架使用到的一种解决方案。
if (log.isDebugEnabled()) {
log.debug("打印调试日志:{}", JSON.toJSONString(goodsList));
}
复制代码
再运行程序,发现效果是显而易见的,耗时变成0毫秒了。
spend time:0
复制代码
这时有人想说,那我们的程序就会像判空那样充斥着大量的 if (log.isDebugEnabled()) 语句了。这时,我们可以借助 Supplier 的延迟加载特性,只有使用时才去计算真正需要的数据,其中某个日志打印方法定义如下:
public void log(Level level, Supplier msgSupplier) {
if (!isLoggable(level)) {
return;
}
LogRecord lr = new LogRecord(level, msgSupplier.get());
doLog(lr);
}
复制代码
其实,有时没必要刻意去使用某个功能点,而是应该要结合实际情况作出合适自己项目的方案,这样才能发挥最大用处。因为如果你对这个功能点了解不深,使用不当,那就适得其反,得不偿失。