Implementing Spring MVC with CDI and Java EE 6 part 1
One of the opinions I’ve had over the last couple of years is that Spring makes things look really easy, and CDI is a great dependency injection framework. Throw in this article suggesting you can build your own Java EE 7 and it sounds like a challenge, so for fun, I thought I might have a go at implementing a subset of Spring MVC on top of CDI with Java EE 6.
Interestingly, there’s nothing like Spring MVC for Java EE which is a shame because it is a really good framework. So in this series of articles, I’ll be covering different aspects of implementing Spring MVC on CDI with the caveat that this won’t be an exact compatible replica. I’ll be implementing just enough of each feature to demonstrate that it can be done, but I’ll leave enough room so that with more work, it can be a more accurate implementation. It goes without saying that these articles assume you are familiar with Spring MVC 3 using annotations and at least a little bit familiar with CDI. I’ll also mention that the code listed in the articles only contains the code relevant to the implementation for demonstration purposes. I’ve removed a lot of the null checks and other defensive programming code to make things more readable but they are still present in the final code.
In this first part, we will start by laying all the dull ground work as we capture the metadata from the MVC annotations in the following steps :
- Define our MVC annotations
- Define a class to hold our request mapped methods
- Write some code to extract the request mapped methods from a given class and store the metadata
The MVC annotations
To get started we implement the controller and request mapping annotations which are based on the Spring MVC versions. @Controller
marks our classes as MVC controllers that implement MVC methods. Each method that can be mapped to a URL is mapped with the @RequestMapping
annotation.
@Target({ ElementType.TYPE,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Controller { }
The request mapping annotation has two attributes (for now), the default value
which contains an array of paths that are mapped to this controller or method. For now, we will implement simple path matching, the url must start with the controller path and end with the path defined on the method. We can change to a more complex wildcard/Ant path matching implementation if we want to later. The other attribute on the request mapping is an array of RequestMethod
types that indicates the type of requests we want to map to this controller and method. For now, we’ll just deal with POST
, GET
and DELETE
request types :
public enum RequestMethod { POST,GET,DELETE }
Now we can define our request mapping annotation :
@Target({ ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RequestMapping { String[] value() default {}; RequestMethod[] method() default {}; }
Armed with our new annotations, we can go ahead and create our first controller :
@Controller @RequestMapping("/person/") public class PersonController { @RequestMapping("list") public String doListPeople() { return "listPeople"; } @RequestMapping("view") public String doViewPerson() { return "viewPerson"; } @RequestMapping(value="edit",method=RequestMethod.GET) public String doGetEditPerson() { return "editPerson"; } @RequestMapping(value="edit",method=RequestMethod.POST) public String doPostUpdatePerson() { return "updatePerson"; } }
For now, our controller methods return a String which can be used to determine the final view to render.
The MVC CDI Handler
Originally, I considered writing this as a CDI extension but felt that it wasn’t a good match and instead chose to create a managed MvcHandler
bean that will be invoked by the servlet and will lazily initialize its own configuration post construction. This led to a more modular design since I could make the MvcHandler
the center of the implementation and it let me write it in a purely managed environment saving me from having to deal with some of the limits extension have compared to managed beans.
Technically, when a web request comes in, we could get a list of controllers and iterate through them to find a matching controller and then search for a method on that controller that matches our request. However, I’m extracting and storing all that metadata on startup since it is more efficient and will allow for more expansion later on. When we start examining different types of metadata, it will get ridiculous doing it all at run time. Also, it lets us optimize a bit since we could have several matches, but some will be better matches than others so we will always need to compare all the controller methods each time a request comes in. Pre-reading it lets us also presort the controller methods into best-fit-first.
Extracting Controller Metadata
We’ll create a class called ControllerInfo
that holds the metadata for the controllers. It does this by taking the controller class and iterating through its methods and seeing if they have the request mapping annotation on them. If they do, those methods are added to a list of ControllerMethod
objects. This object holds info about a specific mapped method on a controller :
public class ControllerMethod { private final String prefix; private final String suffix; private final RequestMethod[] requestMethod; private final Method method; //getters and setters }
The prefix is determined from the request mapping path on the controller class, and the suffix is determined from the request mapping on the method. The request method value indicates which kinds of request methods this method can be mapped to (GET
or POST
requests). The Method
object is a java reflection Method
instance that we can use to not only define the class method that is mapped, but also reference the declaring class for when we need to locate the controller bean. Since the path value can be an array of strings if a particular method has multiple paths in the value then there will be mulitple ControllerMethod
entries, one for each path as the suffix. However, we don’t split up multiple request method values instead opting to store them as they are as an array.
@Controller @RequestMapping("/somePath/") public class MyController { @RequestMapping(value={"page1.do","page2.do"},method={GET,POST}) public void someMethod() { ... } }
The above class results in two entries :
Prefix | Suffix | Request Methods | Method |
---|---|---|---|
/somePath/ |
page1.do |
GET,POST |
MyController.someMethod() |
/somePath/ |
page2.do |
GET,POST |
MyController.someMethod() |
The reason for this is it lets us optimize the path matching based on the best match first by sorting based on the length of the suffix path. Again, we can improve this ordering later on by using a better path matcher.
Now we’ve a place to store the controller method information, we can implement the ControllerInfo
class which contains the list of Controller method instances:
@ApplicationScoped public class ControllerInfo { private final List<ControllerMethod> controllerMethods = new ArrayList<ControllerMethod>(); private static final String[] DEFAULT_MAPPING_PATHS = new String[] { "" }; public static final AnnotationLiteral&lt;Controller&gt; CONTROLLER_LITERAL = new AnnotationLiteral&lt;Controller&gt;() { private static final long serialVersionUID = -3226395594698453241L; }; @Inject private BeanManager beanManager; @PostConstruct public void initialize() { Set&lt;Bean&lt;?&gt;&gt; controllers = beanManager.getBeans(Object.class,CONTROLLER_LITERAL); for (Bean&lt;?&gt; bean : controllers) { add(bean.getBeanClass()); } sortControllerMethods(); } private void add(Class&lt;?&gt; clazz) { } }
The initialize method is invoked when this bean is first created. As we’ll see later, this bean is injected into the MvcHandler
so this method is only called when needed. It uses the CDI API to locate all beans with a controller qualifier annotation and calls the add(Class<?> clazz)
method for each controller class. Finally it makes a call to sort our list of ControllerMethod
instances.
In the add
method, for the given controller class, we iterate through its methods and see if we can add them as a request mapped method.
private void add(Class&lt;?&gt; clazz) { if (clazz.isAnnotationPresent(Controller.class)) { Method[] methods = clazz.getMethods(); String[] controllerPaths = null; RequestMapping rm = clazz.getAnnotation(RequestMapping.class); if (rm != null) { controllerPaths = rm.value(); } // if no paths are specified, then default to one blank path so we always add it if (controllerPaths == null || controllerPaths.length == 0) { controllerPaths = DEFAULT_MAPPING_PATHS; } // add methods for each mapped path at the controller level for (String prefix : controllerPaths) { for (Method m : methods) { addMethod(prefix, m); } } } } private void addMethod(String prefix, Method javaMethod) { RequestMapping mapping = javaMethod.getAnnotation(RequestMapping.class); if (mapping != null) { String[] paths = mapping.value(); // if these are blank, fill with defaults if (paths == null || paths.length == 0) { paths = DEFAULT_MAPPING_PATHS; } for (String path : paths) { controllerMethods.add(new ControllerMethod(javaMethod, prefix,path, mapping.method())); } } }
The addMethod
function will check for a request mapping annotation on the method, extract the metadata and store it in the list.
This about wraps up the first installment which covers the extraction of request mapped methods and storing them for use later on. Next time we’ll start coding our servlet and core MVC handler that will let us make web requests and invoke the mapped methods on our controller before displaying the appropriate web page.
2 thoughts on “Implementing Spring MVC with CDI and Java EE 6 part 1”
Comments are closed.
I have taken a different approach and just extended JAX-RS with a data binder for forms[1]. I think it’s a more feasible approach. Take a look at a sample controller[2].
[1] https://github.com/gsapountzis/samson/
[2] https://github.com/gsapountzis/samson/blob/master/examples/samson-example-jsp/src/main/java/samson/example/jsp/resources/ProductsResource.java
Hey George,
Yeah, there’s a lot of similarities with JAX-RS and you’re right, going that route gets you further much quicker. However, you can’t use the same Spring MVC syntax, and part of the point of this is to see how easy it is to implement a major framework on top of Java EE. I also get the feeling that there’s probably a wall at some point on how flexible Jax-RS is going to be in providing all the features needed.
Cheers,
Andy