美文网首页系统性能优化
减少SpringBoot项目启动耗时(1)— 缩小扫描范围

减少SpringBoot项目启动耗时(1)— 缩小扫描范围

作者: 小胖学编程 | 来源:发表于2022-05-22 10:51 被阅读0次

    背景:由于项目依赖的包越来越多,且配置的@SpringBootApplication(scanBasePackages = "com")扫描范围为顶级目录,就会导致依赖包中的一些无用bean被加载到Spring容器中,从而造成项目启动缓慢。

    1. 背景

    实战模拟.png

    由上图看到,我们本身项目只扫描com.tellme,但是为了开发简便,所以配置的扫描路径为com,也就是后续我们引入com.test的目录,也会被扫描到Spring容器中。

    注:为了便于测试,com.test的目录可以看做为一个新的jar依赖。

    如果我们在启动类直接缩小扫描范围的话,便会存在两种情况:

    @SpringBootApplication(scanBasePackages = "com.tellme")
    
    1. 扫描的包中依赖未被扫描包中Bean时,启动时会抛出异常;
    2. 若是依赖注入的时候存在@Lazy注解,那么在运行时会抛出异常;

    com.test包下面的Bean:

    @Service
    public class TestBean {
        @Bean
        public String test() {
            return "success";
        }
    }
    

    com.tellme包下面的Bean:

    @Slf4j
    @RestController
    public class TestController {
        @Autowired
        @Lazy
        private TestBean testBean;
    
        @GetMapping(value = "/tc/t1")
        public String tt() {
            return testBean.test();
        }
    }
    

    启动时不会抛出异常,但是在运行时会抛出异常。

    结论:贸然的去缩小扫描范围,是存在很大风险的!!!

    2、解决方案

    设计图.png

    实现代码:

    SpringBoot启动类:

    @MapperScan("com.tellme.mapper")
    @SpringBootApplication(scanBasePackages = "com.tellme")
    @ServletComponentScan
    @Slf4j
    public class TestApplication {
    
        public static final TestApplication APP = new TestApplication();
        private ConfigurableApplicationContext applicationContext;
    
        public void setApplicationContext(ConfigurableApplicationContext applicationContext) {
            this.applicationContext = applicationContext;
        }
    
        public ConfigurableApplicationContext getApplicationContext() {
            return applicationContext;
        }
    
    
    
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(TestApplication.class, args);
            APP.setApplicationContext(context);
            log.info("total bean count {}", context.getBeanDefinitionNames().length);
        }
    }
    

    执行main方法接口

    @Slf4j
    public class SpringBeanCheckerV2 {
        public static final SpringBeanCheckerV2 SPRING_BEAN_CHECKER = new SpringBeanCheckerV2();
        private ConfigurableListableBeanFactory beanFactory;
        private final Map<String, BeanDefinition> commonBeans = new LinkedHashMap<>();
    
        private String[] targetPackages;
        private final Map<String, AutowiredFieldClassAndBeanDefinition> notRegisterDependency = new LinkedHashMap<>();
    
        /**
         * 依赖注入的注解
         */
        private final List<Class<? extends Annotation>> annotationClz = Arrays.asList(Autowired.class, Resource.class);
    
        /**
         * Class转化为Bean的注解
         */
        private final List<Class<? extends Annotation>> annotationBean =
                Arrays.asList(Service.class, Component.class, Bean.class, RestController.class,
                        Controller.class, RestControllerAdvice.class, Repository.class, Configuration.class, Mapper.class);
    
        private final Queue<Entry<String, BeanDefinition>> beans = new ArrayDeque<>();
    
        public static void main(String[] args) throws ClassNotFoundException, IOException {
            //启动项目,加载com的Spring容器
            TestApplication.main(args);
            final ConfigurableApplicationContext applicationContext = TestApplication.APP.getApplicationContext();
            SPRING_BEAN_CHECKER.beanFactory = applicationContext.getBeanFactory();
            //        SPRING_BEAN_CHECKER.projectBasePackage = TestApplication.class.getName().substring(0, bootStrapClassName
            //        .lastIndexOf("."));
            SPRING_BEAN_CHECKER.targetPackages = new String[] {"com.tellme"}; //如果有其他包,在这里补充
            SPRING_BEAN_CHECKER.findProjectRegisterBeans();
            SPRING_BEAN_CHECKER.findNotRegisterDependency();
            SPRING_BEAN_CHECKER.printNotRegisterDependency();
            System.exit(1);
        }
    
        public void findProjectRegisterBeans() throws ClassNotFoundException, IOException {
            for (String packageName : targetPackages) {
                findAllBeansByPackage(packageName, annotationBean);
    
            }
        }
    
    
        /**
         * 找到@Service类型注解的Bean放入到commonBeans中;
         * 找到@Configuration类型注解的Bean放入到configurationBeans中--->然后转化为@Bean放入到
         */
        private void findAllBeansByPackage(String packageName,
                List<Class<? extends Annotation>> annotationBeans) throws IOException, ClassNotFoundException {
            ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses().parallelStream()
                    .filter(clazz -> clazz.getPackageName().startsWith(packageName)).map(ClassInfo::load)
                    .filter(c -> annotationBeans.stream().anyMatch(c::isAnnotationPresent))
                    .flatMap(clazz -> Arrays.stream(beanFactory.getBeanNamesForType(clazz))).forEach(beanName -> {
                        //普通Bean的处理
                        final BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
                        commonBeans.put(beanName, beanDefinition);
                        //特殊Bean的处理
                        try {
                            if (getClassByBeanDefinition(beanDefinition).isAnnotationPresent(Configuration.class)) {
                                commonBeans.putAll(findManualConfigBeans(beanDefinition));
                            }
                        } catch (ClassNotFoundException | NullPointerException e) {
                            log.info("beanName {} beanDefinition {} beanClassName not validate", beanName, beanDefinition);
                        }
                    });
        }
    
        private Map<String, BeanDefinition> findManualConfigBeans(BeanDefinition configBeanDefinition)
                throws ClassNotFoundException {
            Map<String, BeanDefinition> beanDefinitionMap = new LinkedHashMap<>();
    
            ReflectionUtils.doWithMethods(getClassByBeanDefinition(configBeanDefinition), method -> {
                if (method.isAnnotationPresent(Bean.class)) {
                    String[] beanNames = beanFactory.getBeanNamesForType(method.getReturnType());
                    for (String beanName : beanNames) {
                        BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
                        beanDefinitionMap.put(beanName, beanDefinition);
                    }
                }
            });
            return beanDefinitionMap;
        }
    
    
        private Class<?> getClassByBeanDefinition(BeanDefinition configBeanDefinition) throws ClassNotFoundException {
            String beanClassName = Objects.requireNonNull(configBeanDefinition.getBeanClassName()).split("\\$\\$")[0];
            return Class.forName(beanClassName);
        }
    
        // 这块查找未注册的Bean使用了广度优先遍历——作为依赖树来进行处理。
        private void findNotRegisterDependency() throws ClassNotFoundException {
            commonBeans.entrySet().forEach(beans::offer);
            while (!beans.isEmpty()) {
                Entry<String, BeanDefinition> beanNameAndDefinitionEntry = beans.poll();
                BeanDefinition beanDefinition = beanNameAndDefinitionEntry.getValue();
                if (beanDefinition == null || StringUtils.isEmpty(beanDefinition.getBeanClassName())) {
                    continue;
                }
                //会读取父类的属性
                ReflectionUtils.doWithFields(getClassByBeanDefinition(beanDefinition),field -> {
                    Map<String, BeanDefinition> dependencyBean = findNotRegisterDependencyBean(field);
                    dependencyBean.entrySet().forEach(beans::offer);
                });
            }
        }
    
        // 递归查类的所有字段,包含父类的字段
        private List<Field> getClassAllFields(Class<?> clazz) {
            List<Field> fields = new LinkedList<>();
            if (clazz == null || "java.lang.Object".equalsIgnoreCase(clazz.getName())) {
                return fields;
            }
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            fields.addAll(getClassAllFields(clazz.getSuperclass()));
            return fields;
        }
    
        /**
         * 存储未被注册,但是要使用的Bean对象
         */
        private Map<String, BeanDefinition> findNotRegisterDependencyBean(Field field) {
            Map<String, BeanDefinition> retMap = new HashMap<>();
    
            if (annotationClz.stream().noneMatch(field::isAnnotationPresent)) {
                return retMap;
            }
    
            Autowired autowiredAnno = field.getAnnotation(Autowired.class);
            if (autowiredAnno != null && !autowiredAnno.required()) {
                return retMap;
            }
    
    
            String[] dependencyBeanNames = beanFactory.getBeanNamesForType(field.getType());
            for (String dependencyBeanName : dependencyBeanNames) {
                if (commonBeans.containsKey(dependencyBeanName) || notRegisterDependency.containsKey(dependencyBeanName)) {
                    continue;
                }
                BeanDefinition dependencyBeanDefinition = beanFactory.getBeanDefinition(dependencyBeanName);
                // Resource 方式注入,指定名称则按名称选择依赖,否则按照primary选择依赖
                final Resource resourceAnno = field.getAnnotation(Resource.class);
                if (resourceAnno != null && StringUtils.isNotBlank(resourceAnno.name())) {
                    if (resourceAnno.name().equals(dependencyBeanName)) {
                        notRegisterDependency.put(dependencyBeanName,
                                new AutowiredFieldClassAndBeanDefinition(field.getType(), dependencyBeanDefinition));
                        retMap.put(dependencyBeanName, dependencyBeanDefinition);
                    }
                } else {
                    if (dependencyBeanNames.length == 1 || dependencyBeanDefinition.isPrimary()) {
                        notRegisterDependency.put(dependencyBeanName,
                                new AutowiredFieldClassAndBeanDefinition(field.getType(), dependencyBeanDefinition));
                        retMap.put(dependencyBeanName, dependencyBeanDefinition);
                    }
                }
            }
            return retMap;
        }
    
        private void printNotRegisterDependency() {
            StringBuilder sb = new StringBuilder();
            notRegisterDependency.entrySet().stream().filter(entry -> Objects.nonNull(entry.getValue().getClazz()))
                    .filter(entry -> Objects.nonNull(entry.getValue().getBeanDefinition().getBeanClassName()))
                    .sorted(Comparator.comparing(entry -> entry.getValue().getClazz().getName())).forEachOrdered(entry -> {
                        final AutowiredFieldClassAndBeanDefinition value = entry.getValue();
                        String beanClassName =
                                Objects.requireNonNull(value.getBeanDefinition().getBeanClassName()).split("\\$\\$")[0];
    
                        sb.append("\n");
                        sb.append("\t@Lazy\n");
                        sb.append("\t@Bean\n");
                        sb.append("\tpublic ").append(value.getClazz().getName()).append(" ").append(entry.getKey()).append("(")
                                .append(")").append(" ").append("{\n");
                        sb.append("\t\treturn ").append("new ").append(beanClassName).append("();\n");
                        sb.append("\t}\n");
                    });
            final String notRegisteredBeans = sb.toString();
            if (StringUtils.isEmpty(notRegisteredBeans)) {
                log.info("没有需要手动注册的Bean,请直接修改SpringBoot启动类的scanBasePackages范围到,{}", targetPackages);
            } else {
                log.info("请在带有@Configuration注解的Class(如果不存在请新建)中增加如下Bean配置:");
                log.info("{}", notRegisteredBeans);
            }
        }
    
        private static class AutowiredFieldClassAndBeanDefinition {
            private Class<?> clazz;
            private BeanDefinition beanDefinition;
    
            public AutowiredFieldClassAndBeanDefinition(Class<?> clazz, BeanDefinition beanDefinition) {
                this.clazz = clazz;
                this.beanDefinition = beanDefinition;
            }
    
            public Class<?> getClazz() {
                return clazz;
            }
    
            public BeanDefinition getBeanDefinition() {
                return beanDefinition;
            }
        }
    }
    
    

    相关文章

      网友评论

        本文标题:减少SpringBoot项目启动耗时(1)— 缩小扫描范围

        本文链接:https://www.haomeiwen.com/subject/dfhkprtx.html