migrating old spring apps
Bringing deprecated Spring routing into the modern world.
Here we go again, with another writeup of dragging Enteprise Java into the modern world. This time, we’re going to be tackling bringing Spring 2.5-based, method-name-resolver controllers and routing into the modern world by removing a dependency on MultiActionController
. MultiActionController
last exists in Spring 3.2.18, which means you’re out of luck if you’re trying to bring an older application forward and it’s heavily dependent on old XML configuration of controllers.
I’m going to describe a general approach here, along with a few suggestions of the best way to move forward. Every application is unique, along with every set of testing needs. I’ll be trying to reference real-appearing code as we go along to provide better example. This guide is going to assume that you have made it up to 3.2.18 and can’t figure out a way forward that doesn’t involve rewriting your entire controller stack in one development iteration.
Step One: Enabling Annotations
We’re moving this application to the future, and the first thing we need to do is let the application know that we’re going to be getting there while still in Spring 3.2.18. If you’ve not moved to annotation or class based configuration yet, this step is as easy as going into one of your web.xml
or servlet.xml
files and adding the following line:
<context:component-scan basePackage="comma separated list of packages, such as us.fournm.application" />
This tells Spring to look in the given package (and its sub-packages) for your annotated controllers and beans. Assuming this file is the same file you have your XML mappings defined in, keep it open, because we’ll soon be deleting most of its contents.
Step Two: Creating a new Base Controller
Due to the removal of MultiActionController, but us not wanting to do a complete rewrite of the frontend, we need a new base class for them to extend. MultiActionController provided routing based on the implementation of handleRequestInternal, and it defaults to routing based on the names of methods in your controller classes, so that’s the main thing we’re going to be emulating.
I’m assuming you’ve got, somewhere in your implementation of handleRequestInternal, some lines that look very similar to the following:
String methodName: super.getMethodNameResolver().getHandlerMethodName(request);
ModelAndView mv: invokeNamedMethod(methodName, request, response);
return mv;
If not, the rest of these steps might not be particularly of use to you, though most other implementations here probably use something similar. Now, to create our new base class:
public abstract class AbstractController {
@RequestMapping
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
// stub
return null;
}
protected String getHandlerMethodName(HttpServletRequest request) {
// stub
return null;
}
protected ModelAndView invokeNamedMethod(String name, HttpServletRequest request, HttpServletResponse response) {
// stub
return null;
}
protected abstract String getDefaultMethodName();
}
Whew! Let’s quickly review–we need handleRequestInternal since that’s what all of our other controllers expect as an entry point, and the @RequestMapping
annotation on it makes it the default pathway for all requests of any HTTP method. We’ll get into the guts of it in a moment. getHandlerMethodName
is our replacement for our bit of it above, though we don’t necessarily need an intermediate MethodNameResolver
this time around. You’re free to use an existing internal one, if it exists, or create your own polyfill to get the method name from the request. invokeNamedMethod
is our actual meat, and quite possibly the most terrifying part of this entire process. getDefaultMethodName
is a best practice, so that we know where to route requests with no information.
Because it’s the most terrifying, invokeNamedMethod
is where we’re going to start. This is the simplest form of this method possible, and it will probably scare you.
protected ModelAndView invokeNamedMethod(String name, HttpServletRequest request, HttpServletResponse response) {
Method method: getClass().getMethodName(name, HttpServletRequest.class, HttpServletResponse.class);
return (ModelAndView) method.invoke(this, request, response);
}
Yep. We grab the method off the class and invoke it. Based on input from a user. That sounds incredibly dangerous, but, it’s already what your code has been relying on if you look at the MultiActionController
implementation of invokeNamedMethod
, more or less. We do have the safeguard that we are expecting a very specific list of parameters on this method, and if they don’t exist (or the method is private) we won’t be able to actually invoke the method. If you rely on the specific additional parameters, such as HttpSession
, you’ll need to write some additional handling around retrieving the method. Similarly, if you’ve got actions that require a command object, you’ll need to handle that as well.
Writing an alternate version of those methods (if they use a standard name) to pass the command along might be the least time consuming action, but it will depend on your application’s code and your consistency in patterns.
Now that the terrifying part is out of the way, we need to implement getHandlerMethodName
–this is likely a parameter of some form passed along in the request. Parse, validate, and return it.
With those pieces, we can put together our basic handleRequestInternal
.
@RequestMapping
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
String methodName: getHandlerMethodName(request);
return invokeNamedMethod(methodName, request, response);
}
A few caveats–be sure to handle any invalid paths, errors, etc. These will be fairly specific to your application, and most of this should be a direct translation from your previous handleRequestInternal
(which you had in a separate base class before, right?), only without routing to a MethodNameResolver
to get the handler method.
Step Three: Migrating Controllers
If you closed it before, it’s time to reopen your servlet mapping file and find a good starting place. Pick a route, copy its URI, and open up the referenced controller.
public class ConcreteController extends MultiActionController { ... }
becomes
@Controller @RequestMapping("path") public class ConcreteController extends AbstractController { ... }
If your controller has no other dependencies, that’s actually it! You’ve finished this controller, as soon as you delete the route and bean definition from your servlet-mapping file! If you do have other dependencies, I would recommend that you take this opportunity to change them to annotation based as well. @Autowired
is your friend here, along with ApplicationContextAware
to cover any controllers that needed Spring’s ApplicationContext
or your ServletContext
.
Finishing up…
If you trust your automated test suite and aren’t going to take this opportunity for any other refactoring, this is the time for doing a bulk change. You might have a few more pieces of functionality to port over from MultiActionController
to make everything happy, though any app of significant size to make this migration painful probably had its own implementation in a class sitting above MultiActionController
to begin with. There’s a few ways to prevent code duplication during this transition period, including pulling things out into utilities or onto interfaces. There are pros and cons for each, depending on the version of Java that you’re able to target.
The advantage of this approach is that you can take your time, spread out over other release cycles or iterations, to bring your entire controller stack to annotated classes. There might be other blockers, but from this point you should be able to move to at least Spring 4’s last release from a webmvc standpoint. Thanks for playing along and I hope this has been helpful!