SpringBoot

Migrating from SpringMVC struts mixed to Spring Boot

Overview

After weeks of work, I have successed in migrating from a Struts/SpringMVC mixed webapp to SpringBoot.

thats 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.
  • integrate eureka, feign with SpringCloud.

Upgrade classical webapp

Upgrade Servelt and Java

  • Servlet: 8.5
  • Java: 8

Maven

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 jars are dependences with 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 unexpected behavior in deserialization.

Manage version with dependency management

Manage the version of 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 a 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 same SpringMVC version with SpringBoot. if it works after test, continue replace all dependences.

Intellij IDEA plugin Dependecy Analyzer will be helpful for exlude jobs.

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] about @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.

migration AppConfig

replace your web.xml

<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
}

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

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.

Remove exclude in maven

just uncomment 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>

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(AppConfig.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.

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