SpringBoot

Migrating from SpringMVC struts mixed to Spring Boot

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.

Refference