After weeks of work, I have succeeded in migrating from a Struts/SpringMVC mixed webapp to SpringBoot.
Overview
That's what we've done
- No code changed on struts, controller and sitemesh
- webapp is still in war format, and won't use embedded tomcat. Because it's hard for Struts/jsp
- Struts, jsp, SpringMVC, thymleaf3(hacking is required) works together with same ognl version.
What's benefit of SpringBoot
- autoconfiguration and spring-boot-starter make works easier.
- Integrates eureka, feign with SpringCloud.
Steps
1. Upgrade Servlet and Java
- Servlet: 8.5
- Java: 8
Full test are required. We test some problems on Mybatis.
2. Maven
2.1. replace local jars with pom
For many old projects, they are transformed from ant to maven years ago, with jars included.
- webapp
-lib
- spring-4.0.9.jar
- mybatis.jar
-src
- java
...
These dependencies are in system scope
<!-- bad practice in maven -->
<dependency>
<groupId>xxxx</groupId>
<artifactId>xxxx</artifactId>
<version>1.3.2</version>
<type>jar</type>
<scope>system</scope>
<systemPath>${project.basedir}/libs/spring-4.0.9.jar</systemPath>
</dependency>
You need to replace the jars with normal dependence. And, full test is required.
if you introduced two jars with different version, it will be dangerous. eg: two jackson will result in unexpected behavior on deserialization.
2.2. Manage version with dependency management
Manage the version of the jar(Eg: guava, common collections) manually is so hard, we can use maven's parent or dependency management to control the version of jars.
Here is an example, you will use springboot as parent without any starter introduced.
<project>
<!-- no jars will be introduced without explicit declare in dependencies -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- we will still use war formated -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- do as springboot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- exclude starters mannually -->
<exclusions>
<exclusion>
<artifactId>spring-boot-starter</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
Now you have the same SpringMVC version with SpringBoot. If it works after test, continue to replace all dependencies.
Intellij IDEA plugin
Dependecy Analyzer
will be helpful for excluding jobs.
3. Migration of the Spring configuration from XML to Java
Since Spring 3, Java configuration is introduced. You can see cookbook here or read books like [Spring in action] on @Configuration
annotation. This is a diff of how to convert the xml into java config.
For most servlet apps, there are two contexts in web.xml, the root context and web context.
3.1. Migration AppConfig
Replace XML based config to Annotation base config.
<web-app xmlns=...>
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>/WEB-INF/root-context.xml</param-value>
- </context-param>
+ <context-param>
+ <param-name>contextClass</param-name>
+ <param-value>
+ org.springframework.web.context.support.AnnotationConfigWebApplicationContext
+ </param-value>
+ </context-param>
+ <context-param>
+ <param-name>contextConfigLocation</param-name>
+ <param-value>com.github.miao1007.web.config.AppConfig</param-value>
+ </context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
Create an AppConfig as your root context.
@Configuration
// see why excluded https://stackoverflow.com/a/18684288/4016014
@ComponentScan(basePackages={"com.github.miao1007"},excludeFilters={
@Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)
@Filter(type=FilterType.ANNOTATION, value=Controller.class)
@Filter(type=FilterType.ANNOTATION, value=RestController.class)
})
// include your old config xml
@ImportResource({"classpath*:root-config.xml"})
public class AppConfig {
// you can also move your root-config.xml's bean to here little by little
}
3.2. Migration WebConfig
Replace your servlet's config in web.xml
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
- <init-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>/WEB-INF/spring/dispatcher-config.xml</param-value>
- </init-param>
+ <init-param>
+ <param-name>contextClass</param-name>
+ <param-value>
+ org.springframework.web.context.support.AnnotationConfigWebApplicationContext
+ </param-value>
+ </init-param>
+ <init-param>
+ <param-name>contextConfigLocation</param-name>
+ <param-value>com.github.miao1007.web.config.WebConfig</param-value>
+ </init-param>
<load-on-startup>1</load-on-startup>
</servlet>
And in WebConfig
@Configuration
@EnableWebMvc
@ComponentScan(basePackages={"com.github.miao1007"}, use-default-filters=“false”,
includeFilters={
@Filter(type=FilterType.ANNOTATION, value=Controller.class)
@Filter(type=FilterType.ANNOTATION, value=RestController.class)
})
//@EnableSwagger2 if you need
@ImportResource({"classpath*:dispatcher-config.xml"})
public class WebConfig extends WebMvcConfigurerAdapter{
//
}
You need to config or override some method in WebMvcConfigurerAdapter , see details like here
4. Get ready for SpringBoot
As far as we do, the webapp is still a classic Spring application, now we will make it as a boot.
4.1. Remove exclude in maven
Just uncommon the starters.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
- <exclusions>
- <exclusion>
- <artifactId>spring-boot-starter</artifactId>
- <groupId>org.springframework.boot</groupId>
- </exclusion>
- <exclusion>
- <artifactId>spring-boot-starter-tomcat</artifactId>
- <groupId>org.springframework.boot</groupId>
- </exclusion>
- </exclusions>
</dependency>
4.2. Mix web.xml with SpringBootServletInitializer
Create a SpringBootServletInitializer
in anywhere, and override configure
method.
@SpringBootApplication
public class AppConfig extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
// this is the "replace" of contextConfigLocation in web.xml
return application.sources(Application.class);
}
}
Remove the root context listener in web.xml
or the context will be initiated twice.
<web-app xmlns=...>
- <context-param>
- <param-name>contextClass</param-name>
- <param-value>
- org.springframework.web.context.support.AnnotationConfigWebApplicationContext
- </param-value>
- </context-param>
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>com.github.miao1007.web.config.AppConfig</param-value>
- </context-param>
- <listener>
- <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
- </listener>
</web-app>
Reload your webapp, now SpringBoot works with a single jar.
Other Frameworks upgrades
Thymleaf3 with struts
When use thymleaf with struts, an error occurred, see issues
Caused by: java.lang.UnsupportedOperationException
Fixed by reflecting the immultableMap to a hash map.
If you want to use them together, go to the issue and find my answer with the price of performance and memory cost.
MyBatis upgrade
In latest MyBatis and Java8, the param keyword #{0}
is removed. See issues
If you are upgrade from an old version of mybatis, please use
# {0} -> #{param1}
In Intellij IDEA, you can press
⌘
+shift
+R
to replace all words with regex.
Conclusion
In this article, we migrated the webapp to SpringBoot step by step. Every step is worth investing even if the migration fails.