一、背景
公司项目引入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实现动态代理,这里就很奇怪。
在这里插入图片描述
查了下SpringBoot的源码,发现是在createAopProxy()中判断使用jdk动态代理还是cglib动态代理,同时对于AdvisedSupport中初始化ProxyTargetClass的值是true,但isProxyTargetClass()拿出来是false,可见在程序运行的过程中ProxyFactory(代理工厂)被修改了。
在这里插入图片描述
根据debug结果可以看到,当前代理工厂的对象是CrudMethodMetadata,查看该对象发现在程序执行过程中重新new了一个ProxyFactory,默认proxyTargetClass是false,所以代理最后是采用了jdk的方式,整体上到这里好像没有什么特别的地方。
在这里插入图片描述
继续查看了getCrudMethodMetadata()的调用,比较特别的地方出现了。可以发现SpringDataJPA是默认支持QueryDsl的,会为QueryDsl类型的接口新生成一个代理工厂,而项目中正好接入QueryDsl实现一些复杂查询。
在这里插入图片描述
回到项目代码,在项目改造初期为了方便操作封装了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左右,基本满足开发、调试和部署。
网友评论