Administrator
发布于 2018-03-08 / 1512 阅读
35

从 SSM 迁移到 Spring Boot 2.0 的第一步与踩坑记录

老大说:新项目别配 SSM 了,直接上 Boot

三月份开新项目,我习惯性地去翻老项目的 spring-context.xml,准备复制粘贴。老大过来一句话把我拦住了:"别配了,用 Spring Boot 2.0。"

我当时是有点慌的——SSM 那套我刚摸熟,web.xml、applicationContext.xml、spring-mvc.xml 三个文件的关系好不容易理清。结果 Boot 说这些都不需要了。花了一周把项目搭起来,记录一下这个过程中的理解和踩坑。

起步依赖:把一堆 jar 版本管理交给它

以前写 pom,最头疼的是版本对齐。spring 4.3.x 配 jackson 2.8.x 配 mybatis 3.4.x,稍微错一个版本就是一上午的 ClassNotFoundException。Boot 的做法是引入 parent:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

parent 里已经用 dependencyManagement 声明好了几百个常用依赖的兼容版本,所以我连 mysql-connector 的版本号都不用写。想看某个依赖最终用的什么版本:

mvn dependency:tree | grep mysql
# [INFO] +- mysql:mysql-connector-java:jar:5.1.46:compile

starter 的命名规律也很好记:spring-boot-starter-* 是官方的,*-spring-boot-starter 是第三方的。一个 starter 本质就是一个 pom,把某个场景需要的一组依赖打了个包。比如 spring-boot-starter-web 里就有 spring-webmvc、tomcat-embed-core、jackson-databind、hibernate-validator 这些,不用自己一个个加。

自动配置到底做了什么

配置完依赖,我写了个最简单的启动类:

@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

跑起来,8080 端口直接就能访问了。Tomcat 哪来的?我明明没配。

关键就在 @SpringBootApplication 这个注解,它是三个注解的合体:

@SpringBootConfiguration   // 就是 @Configuration
@EnableAutoConfiguration   // 核心
@ComponentScan             // 扫描当前包及其子包

自动配置的入口是 @EnableAutoConfiguration,它 import 了 AutoConfigurationImportSelector。这个类会去读所有 jar 包里的 META-INF/spring.factories 文件,把 EnableAutoConfiguration 这个 key 下面列的全限定类名加载进来。我把 spring-boot-autoconfigure 的 jar 解开看了一眼:

# spring-boot-autoconfigure-2.0.1.RELEASE.jar!/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
...(共 100 多个)

那是不是这 100 多个配置类全生效了?不是。每个自动配置类上都挂着一堆条件注解:

@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public class EmbeddedTomcatAutoConfiguration {
    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory() { ... }
}

@ConditionalOnClass(Tomcat.class) 的意思是 classpath 里能找到 Tomcat 这个类才生效——因为我引了 starter-web,Tomcat 被带进来了,所以生效。@ConditionalOnMissingBean 是"你自己没配同类 Bean 我才配",这就是为什么我一旦自己写了个 @Bean DataSource,Boot 的 DataSourceAutoConfiguration 就退让了。

想看自己的应用里到底哪些自动配置生效了,启动时加 --debug

java -jar order.jar --debug

控制台会打出一份报告,分 Positive matches(生效的)和 Negative matches(没生效的,还会写明原因)。我第一次看到这个报告挺震撼的,以前 SSM 里那些黑盒般的 Bean 现在全摊开给我看了。

web.xml 没了,那 Filter 和 Listener 怎么配

这是迁移时最实际的问题。老项目 web.xml 里有字符编码 Filter、一个自定义的登录拦截器。Boot 里改成用 Bean 注册:

@Bean
public FilterRegistrationBean<CharacterEncodingFilter> encodingFilter() {
    FilterRegistrationBean<CharacterEncodingFilter> bean = new FilterRegistrationBean<>();
    CharacterEncodingFilter filter = new CharacterEncodingFilter();
    filter.setEncoding("UTF-8");
    filter.setForceEncoding(true);
    bean.setFilter(filter);
    bean.addUrlPatterns("/*");
    bean.setOrder(1);
    return bean;
}

@Bean
public ServletListenerRegistrationBean<MyListener> myListener() {
    return new ServletListenerRegistrationBean<>(new MyListener());
}

拦截器(Interceptor)不属于 Servlet 规范,是 Spring MVC 的东西,得用 WebMvcConfigurer:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/static/**");
    }
}

注意 Spring Boot 2.0 里 WebMvcConfigurerAdapter 已经废弃了,1.5 时代的很多教程还在用那个抽象类。2.0 因为接口支持了 default 方法,直接 implements WebMvcConfigurer 就行。

还有个配置上的变化我顺带记一下:1.5 里常用的 server.context-path 在 2.0 改成了 server.servlet.context-path。老配置不会报错,只是静默失效,访问路径全变成根路径,Nginx 转发过去就是 404。这种"改了名但不报错"的配置项在 2.0 里有十几个,官方有个 spring-boot-properties-migrator 模块,加进去启动时会把废弃配置打在日志里,升级的时候建议先装上。

踩到的坑

坑一:启动类位置导致 404

我把 Application 类放在了 com.example.app,Controller 放在 com.example.controller,启动没问题但访问全是 404。原因是 @ComponentScan 默认只扫启动类所在包及其子包com.example.controller 是平级包,扫不到。解决办法是把启动类挪到最外层的 com.example 下。

坑二:application.properties 里的中文乱码

在 properties 里写了中文配置,读出来是问号。Spring Boot 2.0 默认用 ISO-8859-1 读 properties。要么改成 application.yml(YAML 默认 UTF-8),要么在 IDEA 里勾上 Transparent native-to-ascii conversion。我选了前者,整个项目切换到 yml 了。

坑三:数据库连接池

1.5 默认用 Tomcat JDBC Pool,2.0 换成了 HikariCP。这个改动没在迁移文档里显著标出来,我是看启动日志里 HikariPool-1 - Starting... 才发现的。Hikari 的配置项前缀是 spring.datasource.hikari.*,老项目的 spring.datasource.tomcat.* 全失效了,最大连接数悄悄回到了默认的 10,压测时直接暴露。

小结

迁移完最大的感受是:Boot 没有发明什么新东西,它只是把"大家都会这么配"的约定固化成了代码。理解了 spring.factories + 条件注解这套机制之后,看任何 starter 都不再神秘。至于那些 XML,说实话我现在想不起来它们的必要性在哪了。

参考