旧 spring 项目如何升级成 spring boot? 探索 spring boot 实现原理
2024-10-12 09:02 阅读(212)

传统 spring 带来的问题

最近接触一个遗留系统,用的是传统的 spring 而不是 spring boot。现在的项目基本都是 spring boot了,只有多年前的项目可能没用 spring boot。spring boot 提供了非常大的便利,升级 spring-boot 势在必行。本文系统通过分析传统 spring 项目如何改造成 spring boot,中间可能的坑,同时去分析 spring boot 的实现原理,也有利于我们后续编写自己的 spring-boot-starter。

用习惯了 spring boot 回头用 spring 觉得难用体现在下面几点。


使用 xml 进行配置,难以动态修改。即使通过变量也不够优雅,需要配置很多变量。

外置 tomcat 启动 web 项目,启动麻烦,速度慢。

引用组件需要单独配置,引用组件较为麻烦。

spring boot 正好解决上面 spring 的问题。


spring boot 时提供外部化配置,通过 .properties 活 .yaml,环境变量等方式配置,比 xml 更简洁。

内置 tomcat 启动,启动速度更快,运行更简单。改成内置启动速度能减少30s(我的项目)。外置 tomcat 需要打包成 war包( 外置tomcat 不需要),先加载 tomcat 再加载 war 包,且用了不同的 classLoader 导致速度变慢。

支持 AutoConfiguration,使得引入组件即能生效,不需要额外的 bean 引入。


spring-boot 和 spring 包上的差异

spring boot 和 spring 的版本并不是一样的,有一个对应关系,可以从这里查到,避免 spring-boot 和 spring 的版本不一致导致兼容性问题。spring boot 和 spring 版本对应表

要改造成 spring boot 并不是换个包就能解决的。因为项目里面有大量的配置和组件,不可能一次就全部改造完毕,需要逐步替换,而且直接换 spring boot 会导致很多 bean 冲突。因此需要深入分析 spring boot 的实现方式,并进行修改。

首先要了解 spring boot 的结构,spring boot 是如何实现上面的功能的。一般使用  spring boot 只要引入 spring-boot-starter。它只是一个包的汇总,并没有代码。下图的灰色框部分是传统的 spring ,而 spring boot 和 spring-boot-autoconfigure 就是基于传统的 spring 开发而来的。spring 的拓展点非常多,使得 spring 框架非常灵活,spring boot 也是基于 spring 的拓展点来实现上面各种各样的功能。

spring boot 的核心部分包含了 spring-boot 和 spring-boot-autoconfigure。两部分的作用不同。

spring-boot 提供了解决上面问题的基础能力,例如内置 tomcat 启动,支持 .properties,.yaml 解析,引导类SpringApplication (main方法里面一开始运行的类,判断是 web 启动还是命令行启动)。但 spring-boot 只提供能力,需不需要还要自己去引用。例如 内置 tomcat 启动,.properties 解析都是通过一个 bean 处理的,但得手动 new 这个 bean 出来。这时候就需要 spring-boot-autoconfigure 来自动注入了。

spring-boot-autoconfigure 首先提供了通过解析 spring.factories 里面的 org.springframework.boot.autoconfigure.EnableAutoConfiguration,并根据 @Conditional 来判断是否要注入某个bean,这是最基础的能力。然后基于前面的能力提供一堆常见的组件的 Configiration,相当于集合了一堆的插件。

在 spring-boot-autoconfigure 包的 META-INF 里面 spring.factories 里面

例如前面提到的内置 tomcat 启动,spring-boot 只提供了类,但要自己转化成 bean 还得自己亲自来,而 autoconfigure 则已提供 AutoConfiguration,可以配置在 spring.factory 里面,并且只要条件满足,自动帮你创建这个 bean。

虽然内置了很多插件很方便,有些只要在 pom 文件 import 就能自动生效,但并不是每个插件都能很好的适配,例如有一个数据库初始化类,DataSourceInitializer,会依赖一个 dataSource,但如果项目是多数据源,会提示 excepted single matching bean but found 2。除此之外会有部分组件因为版本不兼容导致启动失败 (旧项目引用的包和 autoconfigure 引用的包版本不一致),因此需要通过 spring.autoconfigure.exclude 去掉大部分的AutoConfiguration。只留下自己需要的。

分析 spring boot 的功能实现

我们将摊开 spring boot 的代码,看看它是如何实现 spring boot 的功能的。

xml 配置改成 .properties

首先是 xml 的配置问题,spring boot 更多 properties 类来配置,并且可以自动映射到 .properties 中。同时也可以把某个要初始化的 bean 变成一种 properties 类型,即可自动赋值并自动创建 bean。例如上面配置的DruidDataSource。在 spring boot 里面可以这么做。

@ConfigurationProperties(prefix='mydruid') 使得可以通过配置 druid.url,druid.username 等来对对象进行赋值,非常方便,不再需要配置 xml 了。

实现原理是通过 ConfigurationPropertiesBindingPostProcessor 对 bean 进行处理,只要解析出有 Configuration,就会设置根据 @ConfigurationProperties 前缀匹配出对应的配置,并赋值给对应的属性。

public class ConfigurationPropertiesBindingPostProcessor
       implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {


    private ApplicationContext applicationContext;

    private BeanDefinitionRegistry registry;

    private ConfigurationPropertiesBinder binder;

    @Override
    public void afterPropertiesSet() throws Exception {
       // We can't use constructor injection of the application context because
       // it causes eager factory bean initialization
       this.registry = (BeanDefinitionRegistry) this.applicationContext.getAutowireCapableBeanFactory();
       this.binder = ConfigurationPropertiesBinder.get(this.applicationContext);
    }


    
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
       // get 方法会筛选出有 @ConfigurationProperties 的 bean 来继续处理
       bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
       return bean;
    }

    // 对 bean 进行参数赋值
    private void bind(ConfigurationPropertiesBean bean) {
       if (bean == null || hasBoundValueObject(bean.getName())) {
          return;
       }
       Assert.state(bean.getBindMethod() == BindMethod.JAVA_BEAN, "Cannot bind @ConfigurationProperties for bean '"
             + bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean");
       try {
          this.binder.bind(bean);
       }
       catch (Exception ex) {
          throw new ConfigurationPropertiesBindException(bean, ex);
       }
    }

上面的 ConfigurationPropertiesBindingPostProcessor 要生效有3种方案,最终都是为了生成这个 Processor,了解不同的方案有助于我们学习如何更好的编写一个插件让使用方更快捷引入。


通过 @Configuration 手动 @Bean 然后 new ConfigurationPropertiesBindingPostProcessor()。

通过 @Import(ConfigurationPropertiesBindingPostProcessorRegistrar::class) ConfigurationPropertiesBindingPostProcessorRegistrar 会生成一个ConfigurationPropertiesBindingPostProcessor 的 beanDefinition,相当于注入一个 Processor 的 Bean。

通过写一个插件, spring.factory 定义生成 ConfigurationPropertiesBindingPostProcessor, 只要引入这个插件包就行。

通过配置 @EnableConfigurationProperties ,会触发 EnableConfigurationPropertiesImportSelector,然后里面初始化一个相当于 @Import(ConfigurationPropertiesBindingPostProcessorRegistrar:: class) 的配置。

spring-boot 里面推荐第3种方式。有2个原因


properties 也是一个bean, 需要加 @Component, 而方法3 会默认把 properties 也变成一个bean, 不需要加 @Component

一个 @Configuration 的类一般会映射一个 @EnableConfigurationProperties,@Configuration 是用来构造 bean 的,而构造 bean 往往需要一个 Properties。例如下面的 autoconfiguation 里面的 rabbitMq 的 AutoConfiguration 也会搭配一个 RabbitProperties。这也是我们写插件可以参考的。

.properties 解析问题

发现配置了 .properties 但属性并没有被设置,原因在于解析 .properties 的 bean 没有被加载进来。因此要去分析 .properties 是在哪里加载的。分析发现配置文件处理是通过一个 Listener 处理的ConfigFileApplicationListener 在项目一开始 Bean 还没初始化的时候会触发ApplicationEnvironmentPreparedEvent 时间,去读取配置生成 MutablePropertySources。对于 spring boot 来说配置读取不是读取一个文件那么简单,它会识别是哪套环境的配置 (Profile),并读取相应的配置文件,还能读取多个配置文件进行组合。

关键在于 listener 是 spring boot 专属的,而且只能通过引导类的方式启动。如果只是通过 ClassPathXmlApplicationContext 或者 外置 tomcat 来启动,是执行不了 listener 的,这也是传统spring 和 spirng boot 的差异,spring-boot 会触发 listener,并读取 .properties 作为配置类添加到 environment 中,而传统 spring 不会,因此需要使用 spring boot 引导类来启动才行。

这个项目包含2个服务,一个是 web 服务,一个是 rpc 服务。web 服务启动是没有引导类的,因为是通过外置 tomcat 的方式启动,而 rpc 服务是有引导类的。要修改也很简单。

从原有的

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath*:META-INF/spring/*.xml");

改成

// 用于加载原有配置文件
@ImportResource(value = "classpath*:META-INF/spring/*.xml")

// 用于启动 AutoConfiguration,加载 spring-boot-autoconfigure 中的插件
// @SpringBootApplication 也包含了此配置,并提供默认的扫描路径,这里不展开
@EnableAutoConfiguration
public class Bootstrap {

    private static final Logger LOGGER = LoggerFactory.getLogger(Bootstrap.class);

    static {
        System.setProperty("catalina.home", ".");
    }

    public static void main(String[] args) {

        SpringApplication.run(Bootstrap.class, args);
    }
}

外置 tomcat 改内置

内置 tomcat 是需要引导类,在引导类中判断是否要通过 web 启动默认是通过判断是否存在 web 相关的类来决定,也可以手动设置。判断条件在 org.springframework.boot.SpringApplication#deduceWebEnvironment 这里。需要依赖 spring-web 和 javax.servlet.Servlet (servlet-api,tomcat-embed-core 都内置了这个类) 才能通过内置 tomcat 启动。

spring-boot-starter-web 包含了几个 web 需要的包,因此直接引用这个包即可。引导类和上面的引导类保持一致即可。

另外需要一些 autoconfigure 的插件才行,否则有 tomcat 也处理不了请求,所以不要把下面的这些 AutoConfiguration 取消掉了。

外置 tomcat 多了一个 web.xml,原本是给 tomcat 加载的,但换成 spring-boot 之后就不应该保留了,需要改成bean 的方式初始化。例如有一些拦截器需要改成下面这种形式.

open class XXXFilter : Filter {

    override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) {}
}

外置组件引用

这里前面已经提到在 autoconfigure 里面会通过加载 AutoConfiguration 和分析各种 Conditional 条件 实现插件的自动引入。所以相关的组件也可以使用这种方式来编写插件,使得引入更加方便。对实现有兴趣的可以查看 EnableAutoConfigurationImportSelector 这个类的源码。

总结

spring boot 的功能还有很多,这里只是提到一部分,其他还有待探索。但经过上面的分析,从中得知 spring boot 的优化在于


提供一些更便捷通过引导类统一 web 和 非web 的启动方式,并提供 applicationListener 的拓展点,properties 属性绑定,内置tomcat启动。

通过 Authconfiguration 实现插件的条件注入,并支持一堆常用的插件,例如 tomcat,dispatchServlet 等等。


作者:_简简单单_

链接:https://juejin.cn