利用函数式接口与泛型提升批量处理的灵活性与复用性

662 阅读5分钟

起因

在日常服务巡检中,系统出现了大量的警告日志。 经过排查,发现原因在于JPA执行数据库查询时,WHERE子句中的IN条件带有过多参数(超过300个),超出了系统设定的预警限制(128个),从而触发了警告。

image.png

为了解决这一问题,需要对现有代码进行优化:

interface JpaTestRepository extends JpaRepository<TestPo, Long> {
    @Query(value = "select * from TestPo test where TestPo.deleted is false and TestPo.id in ?1")
    List<TestPo> findTestInId(List<Long> idList);
}


@Service
public class JpaTestDao {
    @Autowired
    private JpaTestRepository jpaTestRepository;
    
    // 当 idList 数量过多时就会引发 warn 告警
    public List<TestPo> batchTest(List<Long> idList){
        return jpaTestRepository.findTestInId(idList)
    }
}

如果在每个方法内部实现对idList的拆分和方法的遍历调用,代码将显得冗长且不优雅,并可能产生大量重复代码。

注意到使用IN查询的方法结构大致一致,入参为List<Long> id,返回值为List<Po>

基于这一特点,计划利用函数式接口和泛型来编写一个通用方法,以便在不同类中调用具有类似签名(尽管方法名不同)的方法,从而优化代码结构,提高代码的可复用性和可维护性。

实现

首先定义一个函数式接口 Executable ,这是 Java 8 引入的一个特性,允许将方法作为参数进行传递。

这个接口定义了一个泛型方法 execute ,接收一个类对象和一个目标方法,并返回目标方法的执行结果。通过使用 @FunctionalInterface 注解,确保这个接口中只有一个抽象方法,从而可以使用 lambda 表达式进行实例化。

/**
 * 用于调用某个类中的指个方法
 *
 * @param <T> 类对象
 * @param <V> 目标执行方法
 * @param <E> 方法返回值
 */
@FunctionalInterface
public interface Executable<T, V, E> {

    /**
     * 执行方法
     *
     * @param t 方法所在的类对象
     * @param v 要执行的方法
     * @return 目标方法的返回值
     */
    E execute(T t, V v);
}

BatchHandleUtil 类是一个批量处理的工具类,提供了一个batchQuery方法,用于对数据进行分批处理。这个方法的核心思想是将一个大的ID列表拆分为多个子列表,然后对每个子列表调用指定的方法进行处理。

package com.alaeat.aio.store.utils.func;

import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * 批量处理工具类
 */
@Slf4j
public class BatchHandleUtil {

    private static final int batchMax = 127;

    /**
     * 批量查询工具方法。
     *
     * @param <T> 实体类型
     * @param <E> 查询结果类型
     * @param t 实体类对象
     * @param executable 可执行的查询操作,接收实体和ID列表并返回结果列表
     * @param idList 需要查询的ID列表
     * @return 所有ID对应的查询结果的列表
     */
    public static <T, E> List<E> batchQuery(T t, Executable<T, List<Long>, List<E>> executable, List<Long> idList) {
        StringBuilder str =new StringBuilder();

        List<E> allResults = new ArrayList<>();

        // 将ID列表拆分为多个子列表,每个子列表的大小不超过batchMax
        List<List<Long>> partitionedIdLists = Lists.partition(idList, batchMax);

        for (List<Long> idSubList : partitionedIdLists) {
            // 执行查询并获取结果
            List<E> batchResults = executable.execute(t, idSubList);
            allResults.addAll(batchResults);
        }
        
        return allResults;
    }

}

在实际应用中,通过静态工具类 BatchHandleUtil 对方法进行分批调用。

以 JpaTestDao 类为例,通过 batchQuery 方法传递 jpaTestRepository 对象、要调用的方法 findTestInId 以及参数 idList ,即可轻松实现对数据库查询的分批处理。


interface JpaTestRepository extends JpaRepository<TestPo, Long> {
    @Query(value = "select * from TestPo test where TestPo.deleted is false and TestPo.id in ?1")
    TestPo findTestInId(List<Long> idList);
}


@Service
public class JpaTestDao {
    @Autowired
    private JpaTestRepository jpaTestRepository;

    public List<TestPo> batchTest(List<Long> idList){
    // 在此处传入jpa对象、要调用的方法、传参即可完成分批次调用操作。对源代码修改幅度小,且能快速对同类型方法进行适配
       return BatchHandleUtil.batchQuery(jpaTestRepository, JpaTestRepository::findTestInId, idList);
    }
}

通过函数式接口实现批量处理带来了多种优势:

  • 代码复用性高:函数式接口和工具类的通用设计使得代码能够在项目中被多次复用,无需为每一个批量处理的任务单独编写逻辑,从而提高了开发效率。
  • 维护简便:由于批量处理的逻辑被集中在工具类中,代码结构清晰,维护和扩展时只需关注工具类本身,减少了对其他代码的影响,维护工作变得更加轻松。
  • 高度适配:这种设计模式具备很强的通用性,能够适用于多种类型的方法调用。只要方法符合 Executable 接口的定义,就可以灵活地通过 batchQuery 实现分批处理,增强了代码的灵活性和适应性。

结束语

这篇文章的目的是抛砖引玉,激发大家的灵感。在面对SQL In语句数据量过大的问题时,除了批量处理,还有很多其他的解决方案,比如使用缓存中间件、临时表等。本文主要想展示的是函数式接口的使用,以及它所带来的高扩展性。

函数式接口自从Java 8引入后,提供了一种全新的编程方式。它允许将方法作为参数传递,使得代码更加灵活和可复用。这种特性不仅让代码更易读、更易维护,也为处理复杂业务逻辑时提供了更多的选择。

如果对函数式接口有独特的见解或创新的想法,欢迎在留言区分享。通过彼此的交流与互动,可以一起探索更多的解决方案和应用场景,使代码变得更加高效和优雅。期待看到讨论和建议,共同促进进步!