记一次JPA项目启动速度优化

一、背景

公司项目引入JPA改造了数据层,方便项目适配多种类型数据库,但也逐渐变得臃肿,模块启动速度缓慢。原先web模块启动一次大概需要14s,而改造后启动一次大概需要44s,不便于项目的开发、调试和部署。

二、猜测

网上查了一下关于项目启动速度优化的一些内容,大部分其实就是减少启动时扫描的包,但检查了整个项目,其实我们的项目包扫描的粒度已经很合适了,没有优化的空间。想到改造前后最大的区别就在于数据层的不同,感觉可能是引入了JPA影响,毕竟JPA是面向接口编程,而且项目中定义的JPA接口有100+个,项目启动时动态代理加载的类比较多,导致启动速度比较慢。有了排查方向,后面就比较好处理。

三、懒加载

因为猜测是项目启动的时候加载的类比较多导致的,于是通过懒加载的方式看一下启动效果。在StartServer用全局懒加载的方式启动项目:

public class StartServer {
     
    public static void main(String[] args) {
     
        //SpringApplication.run(StartServer.class, args);
        SpringApplicationBuilder builder = new SpringApplicationBuilder(StartServer.class);
        builder.lazyInitialization(true).run(args);
    }
}

应用全局懒加载,项目从原先44s减少到30s,整体减少了10s+,但因为懒加载的方式,项目启动后有些页面会在首次访问时有几秒钟页面空白的情况,用户体验感不好。同时懒加载本身也会存在一些问题,可能会导致有一些类没法在项目启动的时候被检查到。整体上,运用懒加载这种方式并不太稳妥。

四、接口优化

问题又绕回到了项目本身。想到项目启动过程一般是做了一些bean的初始化和注册操作,于是写个钩子函数,查看是那些bean消耗的时间比较多。工具打印出实例化操作大于100ms的bean:

@Component
public class TimeBeanPostProcessor implements BeanPostProcessor {
     
    private static final Map<String, Long> costMap = new ConcurrentHashMap<>();
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
     
        costMap.put(beanName, System.currentTimeMillis());
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
     
        try {
     
            long start = costMap.get(beanName);
            long cost  = System.currentTimeMillis() - start;
            if (cost > 100) {
     
                System.out.println("class: " + bean.getClass().getName()
                        + " bean: " + beanName
                        + " time: " + cost);
            }
            return bean;
        }catch (Exception e){
     
            e.printStackTrace();
        }
        return bean;
    }
}

启动后发现耗费时间比较多的都是JPA相关的一些接口,也印证了上面对JPA动态代理影响的猜想。根据打印出来的内容可以发现动态代理都是通过jdk去实现,但SpringBoot2.x是默认cglib实现动态代理,这里就很奇怪。
记一次JPA项目启动速度优化_第1张图片
查了下SpringBoot的源码,发现是在createAopProxy()中判断使用jdk动态代理还是cglib动态代理,同时对于AdvisedSupport中初始化ProxyTargetClass的值是true,但isProxyTargetClass()拿出来是false,可见在程序运行的过程中ProxyFactory(代理工厂)被修改了。
记一次JPA项目启动速度优化_第2张图片
根据debug结果可以看到,当前代理工厂的对象是CrudMethodMetadata,查看该对象发现在程序执行过程中重新new了一个ProxyFactory,默认proxyTargetClass是false,所以代理最后是采用了jdk的方式,整体上到这里好像没有什么特别的地方。
记一次JPA项目启动速度优化_第3张图片
继续查看了getCrudMethodMetadata()的调用,比较特别的地方出现了。可以发现SpringDataJPA是默认支持QueryDsl的,会为QueryDsl类型的接口新生成一个代理工厂,而项目中正好接入QueryDsl实现一些复杂查询。
记一次JPA项目启动速度优化_第4张图片
回到项目代码,在项目改造初期为了方便操作封装了JPA和QueryDsl的接口,也正好是这个接口,使得每一个实现它的接口会被代理2次,一次是JPA原生的代理,一次是QueryDsl的代理,而QuerydslPredicateExecutor提供的方法里JpaRepositoryImplementation里都有,没有必要重复引入,去掉就好了。

//修改前
@NoRepositoryBean
public interface CoreDao<T, ID extends Serializable> extends JpaRepositoryImplementation<T, ID>, QuerydslPredicateExecutor<T> {
     
}
//修改后
@NoRepositoryBean
public interface CoreDao<T, ID extends Serializable> extends JpaRepositoryImplementation<T, ID> {
     
}

重启项目,启动时间减少为27s左右,基本满足开发、调试和部署。

你可能感兴趣的:(开源框架,QueryDsl,项目优化,jpa)