函数式编程正简化你的代码
前言
白天在做可视化大屏的接口优化,一共六个子页面对应着六个数据请求接口,现只对其中业务相似的五个接口进行讨论。五个接口总共涉及到三个参数:(1)areaCode—地区编码 (2)areaLevel — 地区层级镇级:2 村级:3 网格级别:4 (3)typeList — 人员类型逗号拼接字符串。
项目经理要求这个大屏的速度要快一些,按理说是应该先优化SQL的,但是这套数据库里面的表结构很乱,包括索引也是很乱的。为了保持和另一个系统查出的数据一致,这个大屏的SQL我都很多是从另一个系统中拷贝来的。既然是可视化大屏,那其实是可以考虑将查询缓慢的数据放入Redis缓存中的,在大领导来参观前,多点一点,之后向领导演示时候就不会卡顿了。
这边简单展示Controller层的以下五个接口:
/**
* 所说的五个接口大致样式
*/
@RestController
@RequestMapping("/important-population")
@Validated
public class ImportantPopulationController extends BaseController {
@Autowired
private Service serice;
/**
* 人群统计
*/
@GetMapping("/population-area-statistic")
public R populationAreaStatistic(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
List<PieChartWithCode> result = service.populationAreaStatistic(areaCode, areaLevel, typeList);
return R.data(result);
/**
* 人口统计
*/
@GetMapping("/age-gender-statistic")
public R ageGenderStatistic(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
BarChart result = service.ageGenderStatistic(areaCode, areaLevel, typeList);
return R.data(result);
}
/**
* 风险等级统计
*/
@GetMapping("/risk-level-statistic")
public R riskLevelStatistic(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
List<PieChart> result = service.riskLevelStatistic(areaCode, areaLevel, typeList);
return R.data(result);
}
/**
* 地区排名
*/
@GetMapping("/area-rank")
public R areaRank(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
List<PieChartWithCode> result = service.areaRank(areaCode, areaLevel, typeList);
return R.data(result);
}
/**
* 重点人员占比
*/
@GetMapping("/area-ratio")
public R areaRatio(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
List<PieChartWithCode> result = service.areaRatio(areaCode, areaLevel, typeList);
return R.data(result);
}
}
问题
接口数据直接放Redis缓存还是较为简单的,系统中已经封装好了相应注解 @RedisCache。
/**
* 事件占比分析
*/
@GetMapping("/case-source-statistic")
@RedisCache(key = "important-population::case-source-statistic", fieldKey = "#year+'-'+#areaCode+'-'+#typeList", expired = 60 * 60 * 24)
public R caseSourceStatistic(@RequestParam String year,
@RequestParam String areaCode,
String typeList) {
}
但是就当前业务来说,数据都存Redis是没有必要的,只有在areaLevel = 2(即查询所有镇级别的数据是才会卡顿,其他村级和网格级的数据情况几乎是不卡顿的)。所以考虑单独处理Redis缓存的存取情况,最简单的方法就是Controller层下的每个方法都写一遍:判断是否查询镇级别数据,根据判断结果决定是否进行Redis存取。但是既然有这么多的共同点,本能的直觉告诉我,一定是能够简化这边的代码逻辑的。
思考
找共同点
五个接口内部都是简单的一个service方法的调用,且参数一致。最重要的相同点是,除了他们都有个service方法调用,我们要对他们做相同的Redis存取逻辑。
不同点
返回类型不同,有List类型也有自定义的类型的。
想法
是否我们能将五个接口重复的进行Redis存取逻辑处理的代码抽取出来,而不关心最后到底是调用哪个sevice方法去查询结果的。在函数式编程中,函数是能够作为参数传递的。那么假如将抽取的那部分重复代码命名为redisHandle,那只要我们对函数redisHandle传入不同的service调用(即函数方法),就能实现redisHandle中动态调用service方法。
实现
选择函数式接口
Java JDK8中自带很多函数式接口Function、BiFunction等,但是这边是三个参数,需要使用 @FunctionalInterface自定义函数式接口
@FunctionalInterface
interface Function3 <A, B, C, R> {
R apply (A a, B b, C c);
}
具体实现
初步实现redisHandle方法
其中redisUtils是对redisTemplate的再封装
redisUtils.get(String key) :直接从redis获取数据
redisUtils.get(String key, Class clazz):从redis获取数据,按指定类型用fastjson解析
由于返回类型不同,使用泛型T
/**
* function:具体的service方法调用
* areaCode:地区编码
* areaLevel:地区层级 镇级:2
* typeList:人员类型
*/
private <T> T redisHandle(Function3 function, String areaCode, Integer areaLevel, String typeList) {
T result;
// redis中存取所用的key
String redisKey = areaCode + "::" + areaLevel + "::" + typeList;
// 只对地区层级为镇级的数据进行Redis缓存
if (areaLevel.equals(2)) {
// redisUtils从Redis中读取已被缓存的数据
result = (T) redisUtils.get(redisKey);
if (result == null) {
// 动态调用具体方法
result = (T) function.apply(areaCode, areaLevel, typeList);
redisUtils.set(redisKey);
}
} else {
result = (T) function.apply(areaCode, areaLevel, typeList);
}
return result;
}
/**
* 只列出一个接口是怎么调用的
*
* 地区人口统计
*/
@GetMapping("/population-area-statistic")
public R populationAreaStatistic(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
List<PieChartWithCode> baseInfoList = redisHandle((a, b, c) ->
populationImageService.populationAreaStatistic((String) a, (Integer) b, (List<String>) c),
areaCode, areaLevel, typeList);
return R.data(baseInfoList);
}
fastjson类型解析出错
以上的方法基本实现了对公共代码的抽取,但是从redis读取数据时候会出现fastjson类型解析异常,redisUtils.get()方法需要传入要解析的目标类型。以下提供了返回类型为List类型和自定义类型BarChart的两种情况的代码。
/**
* function:具体的service方法调用
* areaCode:地区编码
* areaLevel:地区层级 镇级:2
* typeList:人员类型
* returnClass: 返回类型
*/
private <T> T redisHandle(Function3 function, String areaCode, Integer areaLevel, String typeList, Class returnClass) {
T result;
// redis中存取所用的key
String redisKey = areaCode + "::" + areaLevel + "::" + typeList;
// 只对地区层级为镇级的数据进行Redis缓存
if (areaLevel.equals(2)) {
// redisUtils从Redis中读取已被缓存的数据
result = (T) redisUtils.get(redisKey, returnClass);
if (result == null) {
result = (T) function.apply(areaCode, areaLevel, typeList);
redisUtils.set(redisKey);
}
} else {
result = (T) function.apply(areaCode, areaLevel, typeList);
}
return result;
}
/**
* 返回类型为List类型的调用
*
* 地区人口统计
*/
@GetMapping("/population-area-statistic")
public R populationAreaStatistic(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
// 多添加了参数 "List.class"
List<PieChartWithCode> resultList = redisHandle((a, b, c) ->
populationImageService.populationAreaStatistic((String) a, (Integer) b, (List<String>) c),
areaCode, areaLevel, typeList, List.class);
return R.data(resultList);
}
/**
* 返回类型为自定义类型BarChart的调用
*
* 性别年龄统计
*/
@GetMapping("/age-gender-statistic")
public R ageGenderStatistic(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
BarChart barChart = getRedis((a, b, c) ->
populationImageService.ageGenderStatistic((String) a, (Integer) b, (List<String>) c),
areaCode, areaLevel, typeList, BarChart.class);
return R.data(barChart);
}
接口的redis的key相同问题
以上代码虽然基本正确,但是还是没有满足业务需要,发现redis进行set操作时候并没有区分四个不同的接口。所以需要在redisHandle方法中添加一个参数businessKey(这边考虑用接口路径简单拼接),以取分出不同接口的数据。
/**
* function:具体的service方法调用
* areaCode:地区编码
* areaLevel:地区层级 镇级:2
* typeList:人员类型
* returnClass: 返回类型
* businessKey:redis业务键,用来区分不同接口的业务数据
*/
private <T> T redisHandle(Function3 function, String areaCode, Integer areaLevel, String typeList, Class returnClass, String businessKey) {
T result;
// redis中存取所用的key
String redisKey = businessKey + "::" + areaCode + "::" + areaLevel + "::" + typeList;
// 只对地区层级为镇级的数据进行Redis缓存
if (areaLevel.equals(2)) {
// redisUtils从Redis中读取已被缓存的数据
result = (T) redisUtils.get(redisKey, returnClass);
if (result == null) {
result = (T) function.apply(areaCode, areaLevel, typeList);
// 设置redis键能够区分是来自不同的业务接口的
redisUtils.set(redisKey);
}
} else {
result = (T) function.apply(areaCode, areaLevel, typeList);
}
return result;
}
/**
* 传入businessKey参数
*
* 地区人口统计
*/
@GetMapping("/population-area-statistic")
public R populationAreaStatistic(@RequestParam String areaCode,
@RequestParam Integer areaLevel,
String typeList) {
// 多添加了参数 "important-population::population-area-statistic",是由接口路径拼接的,能唯一标识该接口
List<PieChartWithCode> baseInfoList = redisHandle((a, b, c) ->
populationImageService.populationAreaStatistic((String) a, (Integer) b, (List<String>) c),
areaCode, areaLevel, typeList, List.class, "important-population::population-area-statistic");
return R.data(baseInfoList);
}
总结
之前函数式接口用的最多的也只是lambda表达式,这次是新的尝试。当然了,redisHandle方法的代码里idea给报了很多警告还没能解决。以后多多用函数式接口,让自己的代码既高效又美观。