Struts 1 to Spring Migration - Strategy
Asked Answered
F

3

9

I have a legacy banking application coded in Struts 1 + JSP Now requirement is to migrate the backend (for now MVC) to Springboot (MVC). Later UI (JSP) will be migrated to angular.

Caveats

1.) Backend is not stateless

2.) Lot of data stored in session object

Approach

  1. Have 2 application running in parallel (Struts and Spring), and share the session object between the two, Storing session in Database, in memory (Redis) etc. This means lot of code changes, as currently session is manipulated across layers of JSP, action, service for every update/fetch

  2. Build complete Spring application and then make it live, which is again not feasible and we can't have user waiting.

  3. Marry Struts 1 and Spring in same app and later Divorce them, and removing struts components progressively.

Question

Is it feasible to have Struts 1 and Spring together in same web application. Can 2 different servlets (ActionServlet & DispatcherServlet exists together), possible if i have 2 different context paths for spring & struts

Currently focus is to migrate MVC layer, service layer will not be an issue.

Also if i need to keep backend design of API to support REST in future, possible if i can design in such way.

Current

JSP -> Struts 1 MVC -> Service Layer -> DB

What we can built

JSP -> Struts 1 MVC <-> JSON Object parser <-> Spring REST MVC -> Service Layer -> DB

Future

Just remove JSP -> Struts MVC

Angular (or any other framework) -> Spring REST MVC -> Service Layer -> DB

Footprint answered 24/7, 2019 at 15:42 Comment(3)
I would argue that sharing the session seems like the smoothest way as you can migrate endpoint by endpoint and have your reverse proxy figure out on a per request basis whether to ask the new or old system. Once an endpoint has been migrated you can remove it in the old application. Having both struts and spring in the same application screams weird behaviour, hard to debug issues and all sorts of other compat issues. From my point of view having two applications has the least risk as I have experienced this approach first hand and don't see as many downsides compared to the other approachesDecennial
@Decennial Right, bang on. Just exploring on public views, for all the scenarios, edge cases. sharing session means lot of code movement, session is currently manipulates at service layer, action and JSP layer.For every fetch/updates , call needs to be changedFootprint
Duh, changing the session in the jsp is indeed a problem and I do feel your pain :) Can't help you with your other approaches though but I know it's hard to even have two spring application contexts next to each other so struts + spring should be even worse. Better pay the performance cost instead of losing your sanityDecennial
W
6

My friend, it is so good to read your question! I lived the same hell you are about to enter using the very same stack...

Approach 3

Honestly I would never try it. The reason would be simple, we wouldn't want the risk of having old project and new project mixing with each other. Libraries from the legacy are very likely to conflict with libraries from the new project (same libraries, different versions) and you would then need to either refactor old code to allow use of the new version, or change libraries completely.

When migrating, you will want to keep your work on the legacy code to a minimal, to none if possible.

Approach 2

The perfect one, but as you said, it won't pay the bills. If you have the cash to work on it, then great, go for it, otherwise, you are for...

Approach 1

Strangulation, that's what worked for me. Start with a working shared login and then move to small functionalities. Think of a tree, you start by removing some small branches, them you move to nodes, until you are able to cut everything. As you remove the small functionalities, they should be made available on the new product (obviously you can't disrupt service otherwise you would go with approach 2).

More specifically, my suggestion is:

Back-end

1) Get the login working. In my case, legacy was all about session, but we didn't want that on the new product. So we implemented a method on the legacy code during login, which would call Oauth from the new product and store in the database the login information, just as you mentioned. The reason for this is on the front-end part of my reply.

2) Define how your legacy and back-end will live together and the resources to have both of them working (ram and CPU to be more precise).

2.1) If by any chance, your legacy runs on tomcat with custom libraries, you might have problems running your new product in a different context. If that happens, my suggestions, go for Docker (just get a close look on memory usage and make sure to limit them on your container(s)).

3) Start very small, replace functionalities related to creating new stuff which hold little to no logic (small crud, such as users, etc) then move to things that have mid-sized logic or that are really ugly on the legacy product and are used on day-to-day basis by your end-users.

4) All of the rest (by the time I left the company, we were not in this phase yet, so I can't provide much info on that).

5) Don't treat this project as a migration only. Get everyone on the page that this a new product. Old code should not be copied and pasted, it should be understood and improved using best practices.

5.1) Unit and integration tests as soon as possible, if your legacy have them, GREAT, compare results to make sure your refactoring didn't break anything or changed the expected outputs. THIS IS ESSENTIAL.

Front-end

1) Once you have the "unified" login working, you will be able to load the pages from the new product as if they were part of the legacy (you could even add a frame on the jsp of the legacy that will load your angular page, we did it and it works like a charm).

1.1) Not cute from a UI/UX perspective to have old and new pages, but it will add value to end-user and provide you with feedback from them once you release the pieces to production. Since your legacy now have access to the token (or whatever auth method you are using, that will be feasible).

2) Define the styles from the beginning. Don't get the job of UI/UX to later (like my team did). The sooner you figure out things such as colors, design, icons, etc, the less time you will waste on meetings that should discuss the release and its impact but are wasted discussing "this is not the color I wanted" or similar. Honestly, get UX defined before UI and make that crystal clear.

3) Design it as if you were designing a micro-services front-end. You might take a lot of time to get to that point, but if and when you do, the migration from the new architecture to micro-services will be much less traumatic.

Culture

I don't know the culture of your workplace, but mine was far from perfect, old people with old thinking into their comfort zones.

Get to change the workplace culture to adapt to what we currently have on the marketplace, old people sometimes tend to resist change, specially when they are technical and do not update on what's new out there. It will make much easier to replace people when they leave the company (because people do move on).

I heard they are still trying to run Scrum (as I mentioned I am no longer there), so there was a huge headache for developers defining What and How the migration of functionalities will take place.

Those are my two cents, hope they help you in some way, and I wish you the best of luck.

Windward answered 24/7, 2019 at 16:24 Comment(1)
equally interesting was your answer :) Thanks for giving so much time to my question and explaining every pros & cons.Footprint
O
1

Since option 2 is not considerable because of business feasibility let’s talk about the other two.

Approach 1

If you push your session state as far back as appropriate datastore ( Redis/Memcache) and use a transparent mechanism to get the session data and update any changes made by app server then you would not need to change any code interacting with session. Any call to get session object from any piece of code in your application is delegated to container and it is container which provides you with the object ( usually a mapping of sessionid and object is maintained). For container such as Tomcat I am aware of session manager which can be replaced by just putting the jar in the container and pointing the config to the backend store. I have used memcache based session manager successfully in production for a high traffic internet application. Check this for Redis (https://github.com/pivotalsoftware/session-managers) and Redisson Tomcat manager (https://github.com/redisson/redisson/tree/master/redisson-tomcat), Memcache (https://github.com/magro/memcached-session-manager) Using this will tranparently fetch/store session in respective datastore without changing any session related code in your application.

So the request with session id in cookie can land up on any of the tomcats ( hosting struts or Spring MVC app) and get the session fetched from the backend store, made changes upon and tranparently stored back.

Approach 3

Is technically possible ( they are after all different servlets with different configuration responding to different url patterns) but opens a lots of problems areas in terms of dependency jars conflicts. But if you freeze the library versions of both the framework during the migration and somehow don't get any conflict with a certain versions of your mix then it can be worth trying as eventually the struts library and it's dependencies will go away.

As for the Angular part - you can still still have the user info in the session and rest of the stateful interaction will need to be designed in a series of stateless ( stateless on the middle tier as eventually you will need some state - just that it will be pushed to the database) interactions.

Oruntha answered 25/7, 2019 at 12:45 Comment(0)
G
1

Since my situation was having a framework that is built on top of struts, I chose Approach 3 so that work can go on while I start putting in Spring-isms to the app. This allowed incremental modernization of the application without throwing the baby with the bath water.

I'll try to put as much information as I can as there were many things that were done.

Initial integration so we can get the Application Context

  1. Add org.springframework:spring-context and org.springframework:spring-web to your dependencies

  2. Create your context configuration class (I am presuming the use of the annotation based configuration not XML). I am also assuming your Spring beans will be defined in the same package.

@Configuration
@ComponentScan
public class MyContextConfig {
}
  1. in web.xml you need to add the Spring web context and request listeners. These will make the ApplicationContext available to the ServletApplicationContext
  <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>myapp.spring.MyContextConfig</param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <listener>
    <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
  </listener>
  1. To get the application context you use from the ServletContext use the following
org.springframework.web.context.support.WebApplicationContextUtils
  .getRequiredWebApplicationContext(servletContext);
  1. To get the application context inside a JSP, the JSP engine provides application as the ServletContext i.e.
org.springframework.web.context.support.WebApplicationContextUtils
  .getRequiredWebApplicationContext(application);

At this point you now have Spring with your Struts app and the JSPs and can take advantage of the dependency injection mechanism.

Datasource integration

One of the first things you'd probably try to provide Spring is your datasource so assuming your Struts app is using the data source provided by the JEE container. In which case add org.springframework:spring-jdbc to your dependencies then

In the MyContextConfig add the following

@Bean
DataSource dataSource() throws NamingException {

  final var jndiTemplate = new JndiTemplate();
  final var ds = jndiTemplate.lookup("java:comp/env/jdbc/MyDB",
    DataSource.class);
  // the TransactionAwareDataSourceProxy allows the datasource to work with
  // Spring managed transactions.  So it gives a bit of interoperability.
  // Spring components like JpaTransactionManager will be aware of this
  // class and pull out the actual data source if needed.
  return new TransactionAwareDataSourceProxy(ds);
}

Actions as Components

At this point you may want to have dependency injection for your Action classes to do that you'd have to set up a RequestProcessor

public SpringRequestProcessor extends RequestProcessor {
  private ApplicationContext applicationContext;
  @Override
  public void init(ActionServlet servlet, ModuleConfig moduleConfig) 
    throws ServletException {

    super.init(servlet, moduleConfig);
    applicationContext = WebApplicationContextUtils
     .getRequiredWebApplicationContext(servlet.getServletContext());
  }

  @Override
  protected Action processActionCreate(
      final HttpServletRequest request,
      final HttpServletResponse response,
      final ActionMapping mapping)
      throws IOException {
    final var className = mapping.getType();
    final var fromSpring = getInstanceFromSpring(className);
    return fromSpring.ifPresentOrElse(
      action ->{
        action.setServlet(servlet); 
        return action;
      },
      () -> super.processActionCreate(request, response, mapping);
    }
  }
  @SupressWarnings("unchecked")
  private Optional<Action> getInstanceFromSpring(String className) {

    try {
      final Class<? extends Action> controllerClass =
          (Class<? extends Action>) Class.forName(className);
      return Optional.of(applicationContext.getBean(controllerClass));
    } catch (final NoSuchBeanDefinitionException e) {
      return Optional.empty();
    } catch (final ClassNotFoundException e) {
      throw new ServletException(e);
    }
  }
}

The struts-config.xml should then specify the processor class in it's controller element

<controller contentType="text/html;charset=UTF-8"
  locale="true"
  nocache="true"
  processorClass="my.SpringRequestProcessor"
/>

The rabbit hole...

From here the rabbit hole may eventually bring you to use spring-data-jpa in which case you'd have to set up quite a number of things including the JpaTransactionManager and maybe wanting to ensure all requests are transactional that is managed by Spring. You'd have to add that as an override to RequestProcessor.processActionPerform. I didn't include that because setting up the JPA is not trivial.

However, with this approach the capabilities can be slowly added to the legacy app with a modern dependency injection framework. For example the legacy system had it's own SMTP and data source management but we swapped it with JPA and Spring Mail (which allowed us to use TLS on email)

You also leverage your existing staff and skills with Struts since the changes to Struts are in fact minimal.

Guanine answered 14/8, 2023 at 6:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.