0
0
0
15,745

How to Build a Custom Dispatcher Servlet and a MVC Framework

jorge.simao
Posted 9/1/10

How to Build a Dispatcher Servlet and MVC Framework

Abstract

I will show how simple is to build you custom MVC framework for webapps. The key implementation concept is the DispatcherServlet, that implements the front-controller design-pattern. The DispatcherServlet is a servlet that first gets client requests, and selects an application controller to dispatch the request to.

Introduction

The Model-View-Controller (MVC) architecture as been widely used in implementing web applications. A Controller is responsible for handling requests and implement application logic, while Views specify the visual layout and navigation structure provided to the client. Controllers may select a view to display or have the underlying infrastructure select one automatically based on the request path. The Model is the link between the controllers and views, and allow view to display dynamic content. A model is usually a map between symbolic name and object values, and is populated by the controller during request handling.

MVC architecture are considered a good design approach as it separates processing and display concerns. And since some kind infrastructure or framework needs to be used to connect the different component together the same infrastructure can be used to factor common functionality across applications Popular frameworks implementing this architecture in Java include Spring MVC and Struts. While you might be know how to use some of these or other framework, its nice to know how they basically work inside. This not only gives more understanding and confidence using a ready-made framework, but also allow you to develop your custom framework if for some reason you prefer to go this way (e.g. you don't to learn about the syntax and meaning of a possible complex set of configurations files, you want to control in detail what happen to client request all the way trough, or you want to guide the framework design to you specific needs).

In this article, I will show how simple is to build you custom MVC framework for webapps. The key implementation concept is the DispatcherServlet, that implements the front-controller design-pattern. The DispatcherServlet is a servlet that first gets client requests, and selects an application controller to dispatch the request to. This is usually done based on the request path and/or the HTTP request method.

Dispatcher Servlet

A DispatcherServlet is the font-end to a Java MVC web architecture. It is a regular Java Servlet but does not have an application specific function. Rather it dispatches request to appropriate application controllers based on request information such a path (URL segment) and HTTP method. Application runnings in a MVC framework don't need several servlet, since more than one controllers can be invoked by the DispatcherServlet.

Java listing below illustrates a first attempt to created DispatcherServlet:


public class DispatcherServlet extends HttpServlet {	
	public void doGet(HttpServletRequest rqst, HttpServletResponse rsp)
	throws ServletException, IOException {
	
	 	String path = rqst.getPathInfo();

		if (path.equals("action1"))
			view = action1(rqst, rsp);
		else if (path.equals("action2"))
			view = action2(rqsr, rsp);
		else { //check for more actions }

		if (view!=null) {
			RequestDispatcher rd = rqst.getRequestDispatcher(view);
			if (rd!=null) { rd.forward(rqst,rsp); }
			else {
				PrintWriter out = rsp.getWriter(); 
				out.printf("Error: Unable to resolve view: %s%n", view);
	  		}
	  	}
	 }
	 
	 public String action1() { return "view1.jsp"; }
	 public String action2() { return "view2.jsp"; }	 
}

DispatcherServlet extends the abstract class HttpServlet. This standard class extends the Servlet class and adds HTTP protocol specific features. In particular, the method service() is implemented to call methods specific for each HTTP request methods (doGet() for HTTP GET method, do Post() for a POST, and so forth). The request path is compared with action names, and an action method is called. On return, a view resource is forward back to the client.

Although fully functional the DispatcherServlet above has a number of drawbacks and limitations. It is coupled with the web application as it implements specific controller actions, it only handles Http GET methods, it is fully bound and limited to view technologies that are supported by the servlet container (e.g. JSP and HTML), it does not simplify the task of getting request parameters, it has not have the notion of model objects and variables to evaluate dynamic views, it requires view resource file to be explicitly returned, it has limited behavior and configuration for error handling, and possibly a few more issues. Thus, to be useful in real applications a DispatcherServlet implementation should consider each of these issues in turn.

Design for an Extensible DispatcherServlet

Java listing below illustrates the basic design for a more general and extensible DispatcherServlet. Many points in the code are left blank for now (marked with ...), as they will be explained and completed as we go through this article. In the implementation of DispatcherServlet we factor processing by invoking method dispatchReply() with first argument specifying the HTTP method.


public class DispatcherServlet extends HttpServlet {	

	public void init() { ... }
		
	public void doGet(HttpServletRequest rqst, HttpServletResponse rsp)
	throws ServletException, IOException {
	   	dispatchReply(HttpMethod.GET, rqst, rsp);
	}

	public void do Post(HttpServletRequest rqst, HttpServletResponse rsp)
		throws ServletException, IOException {
	   	dispatchReply(HttpMethod.Post, rqst, rsp);
	}

	...
			  
	public void dispatchReply(HttpServletRequest rqst, HttpServletResponse rsp)
		throws ServletException, IOException {
		PrintWriter out = rsp.getWriter(); 

	 	String path = rqst.getPathInfo();
		HandlerContex hctx = new HandlerContex(path, meth);
	
		Object r = null;
		try {
			r = dispatch(hctx, rqst, rsp);
		} catch (Exception ex) {
			sendExceptionError(ex, hctx, rqst, rsp);		
			if (debug) ServletUtil.info(rqst, rsp);
			return;
		} 
		if  (!hctx.isDone()) {
			sendMappingError(hctx, rqst, rsp);
		} else {
			reply(r, hctx, rqst, rsp);
		}			 	
	}
	...
}

Method dispatchReply() performs two high-level steps involved in request handling. First, it invokes method dispatch() where an application controller should be called. Secondly, it invokes method reply() to send some view to the client for display. The implementation of these two methods is described further below. Exception thrown or raised during the execution of the controller are handled by calling sendExceptionError().

Figure, below shows a diagram of the MVC framework architecture that we will describe in this article.

Architecture for a custom MVC Framework.

Dispatching Requests and Invoking Handlers

Method dispatch() is shown in listing below. It calls method getHandler() to get a Handler object willing to process the request, and call method invoke(..) on that object. A Handler object is an adapter that bind the MVC framework the application code. It may be directly implemented by an application, or it may adapt a POJO (Plain-old Java object) to handle requests. Method Handler.invoke(..) is where the actual handling of the request is done.

	
	public Object dispatch(HandlerContex hctx,
		HttpServletRequest rqst, HttpServletResponse rsp)
		throws ServletException, IOException {

		Handler h = getHandler(hctx, rqst);
		if (h!=null) {
			return h.invoke(hctx, rqst, rsp);
		}
		return null;
	}
	
	 Handler getHandler(HandlerContex hctx, HttpServletRequest rqst) {
		return ...;
	}   

Method getHandler(..) may implemented with a variety of strategies. Typically, Handler objects should be registered with some high-level Handler container, that select one specific Handler based on the request path. Details of possible strategies are described further below.

Request Handling Context

Note, that an object of type HandlerContext is created as passed around as argument to other methods. This object is used to hold request processing state, such as the parts of the request URL already consumed and the request handling status. In particular, method HandlerContext.isDone() is used to check if the request was successfully handled by an application controller. If this is not the case, then method sendMappingError() is invoked to send an error view to the client instead of regular application view.

A possible implementation of an HandlerContext object is shown below. It holds a property for HTTP the request method, and a reference to the request path. The request path is split around token character / so that path segments can be get and consumed individually. A custom split method is used to make sure that empty path segments are removed. Boolean flag done holds the request handling status. It should be explicitly set by handling objects if they handle the request, so that no further invokation of application code are done.

Method getPathSeg() is used to get the path segment to process. Methods nextPathSeg() and prevPathSeg() are used to consume or roll-back path segments.

	
public class HandlerCtx {
	String path;
	public String[] pathseg;
	public int idx;
	boolean done;
	HttpMethod meth;	
	
	public HandlerCtx(String path, HttpMethod meth) { 
		setPath(path);
		setHttpMethod(meth);
	}
	
	public void setHttpMethod(HttpMethod meth) {
		this.meth = meth;
	}

	public HttpMethod getHttpMethod() {
		return meth;
	}

	public void setPath(String path) {
		this.path = path;
		this.pathseg = null;
		idx = 0;
		done = false;
		pathseg = split(path, "/");
	}
	
	static String[] split(String s, String sep) { ...
}

	String getPathSeg(boolean advance) {
		if (pathseg==null) return null;
		if (idx>=pathseg.length) return null;
		String s = pathseg[idx];
		if (advance && idx<=pathseg.length) idx++;
		return s;
	}		

	String getPathSeg() { return getPathSeg(false); }	
		
	String nextPathSeg() { return getPathSeg(true); }
	String prevPathSeg() {
		if (pathseg==null) return null;
		if (idx-1<0) return null;
		return pathseg[idx-1];		
	}
	
	void setDone(boolean done) { this.done = done; }	
	boolean isDone() { return done; }
}


It is important to point out, that it is not strict necessary to use a HandlerContext object to implement the MVC framework and the DispatcherServlet. Since the HttpServletRequest object for the request is moved around one can always get the request path. Furthermore, there is other ways to check if a controller was not found. For example, handler objects might have a method to check first if they are willing to handle requests. This solution, however, may lead to repeated processing first for checking the request and next for the actual processing. Using a status object such as HandlerContext allows controller matching and request handling to be performed in a single step. It also useful to encapsulate other request handling state.

Interpreting Handler Return Values

Notice that dispatch() returns a value object. This is often the same object as returned by the invoked application controller, and is also the value that is passed as argument to method reply(). The type and and value of this return value is interpreted to get a named for a view to send to the client. Alternativelly, it may be a model object.

	
	public void reply(Object r, HandlerContex hctx,
	HttpServletRequest rqst, HttpServletResponse rsp)
	throws 	IOException, ServletException { 
		PrintWriter out = rsp.getWriter(); 
		
		if (r==null) {
			//auto select view name
  			String view = selectViewName(hctx, rqst);
			viewOut(view, r, rqst, rsp);
	
		} else {
			if (debug) out.printf("<hr/><p>Response: %s</p><hr/>%n", r);
			if (r instanceof String) {
				String viewName = (String) r;
				viewOut(viewName, null, rqst, rsp);
			} else if (r instanceof ModelView) {
				ModelView mv = (ModelView) r;
				viewOut(mv.getView(), mv.getModel(), rqst, rsp);
			} else {
				//auto select view name; use response as single model object
				String viewName = selectViewName(hctx, rqst);
				viewOut(viewName, r, rqst, rsp);
			}
		}		
	}

If the value passed to reply is null, then a view name is automatically selected based on the request path. This is encapsulated in method selectViewName(..). If the value is of type String then it is interpreted as logical view name. If thevalue is of type ModelView then the view and model is get directly for this object. If an object of some other types is passed, then it is interpreted as the single model object. Methods viewOut(..) is used to delegate the work of actually sending the processed view back to the client. (Detailed further below.)

Listing below show one possible implementation of selectViewName(), to handle the case where the application controller does not provide an explict View or view name. It simple takes a segment from the request path and uses this as view name. A more general approach would be to use some kind of configurable ViewSelector strategy object. (Not shown)

	
public String selectViewName(HandlerCtx hctx, HttpServletRequest rqst) {
	String pathseg = hctx.prevPathSeg();

   	if (pathseg!=null) return pathseg;
   	return null; //TODO: return error view name
}

Sending Views

Method viewOut(View vw, ..) take a View object as argument and calls method render() to ask the view to write its content to the output stream of the HttpServletResponse. A model object is passed as argument so that dynamic view can be processed and evaluate expressions referencing model variables.
	
public void viewOut(View vw, Object mdl,
	HttpServletRequest rqst, HttpServletResponse rsp, boolean include)
	throws ServletException, IOException {
		try {
			vw.render(mdl, rqst, rsp, include);
		} catch (Exception ex) {
	  		sendViewError(vw, rqst, rsp);		
		}
	}
}

Another viewOut(String viewName, ..) is used to send a named view. It generates the name of an actual view resource (filename) from the view logical name by calling selectViewFile(), creates a View object og the default type (JspView for JSP content), and defers further processing to method viewOut(View view, ..).

The generated view name by done by inserting a .jsp extension to the logical view name. One or more directories may also be searched for file existence before returning a filename (not shown). View files are also prefixed with / to make sure that view resource is not redirected again to the DispatcherServlet by the servlet container.

	
public void viewOut(String viewName, Object mdl,
	HttpServletRequest rqst, HttpServletResponse rsp, boolean include)
	throws ServletException, IOException {  
		String viewFile= selectViewFile(viewName);
   
		if (viewFile==null) {
			sendViewError(viewFile, rqst, rsp);
			return;
		}

		View vw = new JspView(viewFile); //TODO: cache view object
		viewOut(vw, mdl, rqst, rsp, include);
	}

	public String selectViewFile(String viewName) {
		if (viewName==null) {
			return null;
		}
		if (viewName.startsWith("/")) {
			return viewName;
		}
		String dir = ""; //""|"WEB-INF/" | "WEB-INF/views/" | "WEB-INF/jsp/" | "jsp/" | "views/"
		return "/" + dir + viewName + ".jsp";
	}
}

Notice that a boolean argument include is passed to viewOut(..) methods. This is used to specify if the view to include is the full content of the response or simply a part of the full response. Views as fragments might be used, for example, if a view template system is used to add automatic decorations to application views (e.g. header, menu, and footer), or if some debug or other content is included automatic in the response by the DispatcherServlet. Two variants of the viewOut(..) abstract this decision, by selecting a value for the include argument.


public void viewOut(String vwfile, Object mdl,
HttpServletRequest rqst, HttpServletResponse rsp)
throws ServletException, IOException {  
	boolean include = debug || useViewTemplates();
	viewOut(vwfile, mdl, rqst, rsp, include);
}


public void viewOut(View vw, Object mdl,
HttpServletRequest rqst, HttpServletResponse rsp)
throws ServletException, IOException {
   	boolean include = debug || useViewTemplates();		
	viewOut(vw, mdl, rqst, rsp, include);
}

Handlers

A Handler object is an adapter that bind the MVC framework with the application code. Its defined as an interface with a single method invoke() where request handling is done. The specification is shown below:


interface Handler {
	public Object invoke(HandlerCtx hctx, HttpServletRequest rqst, HttpServletResponse rsp)
	throws Exception;
}

Once possible approach is for application controllers to implement the Handler interface directly. An example is show below:


class MyController implements Handler {
	public Object invoke(HandlerCtx hctx, HttpServletRequest rqst, HttpServletResponse rsp)
	throws Exception {
		if (hctx.getPatgSeg.equals("action1")) {
			Object r = action1();
			hctx.setDone(true);
			return r;
		}
		...
	}
	
	String action1() { ... return "view1"; }
	void action2() { ... }
}

It is often preferred to have application code be set as POJO with minimal or no dependency on the MVC framework. This is facilitates automation of other tasks such as extracting request parameters, and creation of a model object. This is further explored below.

Method Handlers

The natural way to adapt application code to the MVC framework is to have a Handler implementation that uses the JAVA reflection package to invoke a specific method of a Controller. A possible implementation is shown in listing below as class MethodHandler. It declares a single constructor that takes a controller object and a method as arguments. Implementation of method invoke(..) call an auxiliary method setupArgs(..) to setup the method arguments (if any), and invokes the method on the object passed in the constructor. Static method may also be invoked by passing a null object in the constructor.


class MethodHandler implements Handler {
	Object obj;
	Method mt;
	Class<?>[] tys;	
	Object[] args;
	
	public MethodHandler(Object obj, Method mt) {
		this.obj = obj;
		this.mt = mt;
		tys = mt.getParameterTypes();
		args = new Object[tys.length];
	}
	
	public Object invoke(HandlerCtx hctx, HttpServletRequest rqst, HttpServletResponse rsp)
	throws Exception { 
		Object[] args = tys.length>0 ? new Object[tys.length] : null;

	 	if (args!=null) setupArgs(tys, args, hctx, rqst, rsp);
		Object r = args!=null ? mt.invoke(obj, args) : mt.invoke(obj);
		hctx.setDone(true);
		return r;
	}
}

Method setupArgs() is where values of arguments for controller are filled-up. In the implementation shown below, it uses the strategy of checking the parameter types to setup the values. This might be values passed in by the DispatcherServlet, newly create objects, or request parameters (e.g. HTTP form parameters).


public static void setupArgs(Class<?>[] tys, Object[] args, HandlerCtx hctx,
HttpServletRequest rqst, HttpServletResponse rsp) 
throws Exception {	
	Enumeration<?> e = rqst.getParameterNames();

	for (int i=0; i<args.length; i++) {
		if (tys[i].isAssignableFrom(HttpServletRequest.class)) { args[i] = rqst; }
		else if (tys[i].isAssignableFrom(HttpServletResponse.class))
			{ args[i] = rsp; }
		else if (tys[i].isAssignableFrom(Model.class))
			{ args[i] = new Model(); }
		else if (tys[i].isAssignableFrom(HttpMethod.class))
			{ args[i] = hctx.getHttpMethod(); } 
		else if (tys[i].isAssignableFrom(HandlerCtx.class))
			{ args[i] = hctx; }
		else {
			Object val = e.nextElement();
			if (val==null) {
				throw new Exception("Null value for handler argument of type:" +
					tys[i].getName());
			}
			if (tys[i].isAssignableFrom(Number.class))
				{ args[i] = TypeUtil.cast(val, tys[i]); }
			else if (tys[i].isAssignableFrom(String.class))
				{ args[i] = val.toString(); }
			else {
				throw new Exception("Unable to setup handler argument of type:" +
				tys[i].getName());
			}
		}
	}		
}

The argument types that can be fill-up directly from the values passed by the DispatcherServlet, are HttpServletRequest and HttpServletResponse, provided by the Servlet container, and the HandlerContext.

Create objects include a Model. This is simply a wrapping for a java.util.Map, but is useful to remove the need to be created explicitly by all controller methods.

Since Java reflection package does not provide any means to know about the names of method arguments, a more indirect approach needs to be used to map Http request parameters to Java method argument names. In the implementation above, request arguments are assumed to be passed in the same order as the method argument. A more realistic implementation may use annotations on method arguments to specify the parameter name, and fall-back to the default strategy if no annotation is specified. (Left as exercise for the reader.)

Class Handlers

Using class MethodHandler directly, an application would need to register a different MethodHandler for each public method in a controller class that implements a request handling action. A more comfortable approch is to have a Handler implementation that is able to handle all (or a sub-set) of the public method of a controller. A (partial) implementation of this is shown in listing below with class ClassHandler.


public class ClassHandler implements Handler {
	Object obj;	
	Map<String, MethodHandler> map = new HashMap<String, MethodHandler>();

	public ClassHandler(Object obj) {
		this.obj = obj;
	}
		
	public Object invoke(HandlerCtx hctx, HttpServletRequest rqst, HttpServletResponse rsp) 
	throws Exception {
		String s = hctx.nextPathSeg();
		if (s==null) return null;
		MethodHandler hm = map.get(s);

		//TODO: set argument types or search by method name
		if (hm == null) {
			Class<?>[] tys = null;
			Method mt;
			try {
				mt = obj.getClass().getMethod(s, tys);
			} catch (Exception e) {
				return null;
			}
			hm = new MethodHandler(obj, mt);
			map.put(s, hm);
		}
		if (hm!=null) {
			Object r = hm.invoke(hctx, rqst, rsp);
			hctx.setDone(true);
			return r;
		}
		return null;
	}
}

Class ClassHandler manages a collection of Handler as a cache map, mapping request path segments to some MethodHandler. When it is invoked it checks the map for a previously created MethodHandler and uses if found. If not, a MethodHandler is created first, saved in the cache map, and invoked afterwards.

The implementation above does not fully deals with method overloading (i.e. methods with the same name, but with different argument types). A possible strategy to implement method overloading is to map request path segments to lists of MethodHandler, and check or invoke each in turn until one is found whose method arguments can by completely intialized. (It is left as an exercise to the reader to implement this.)

Handler Containers

Most often an application will have more than one controller, so we would like to have a Handler container that dispatches request to one among several application controllers. Since one may use multiple strategies to implement such a container, we define an interface HandlerMapping for the basic service. A possible design for HandlerMapping is shown in listing below. It defines a single method getHandler(..) that return an appropriate handler for a request.


public interface HandlerMapping extends Handler {
	Handler getHandler(HandlerCtx hctx, HttpServletRequest rqst);
}

Note that we made HandlerMapping extends the interface Handler, so that we may call invoke() on it. This suggests an implementation for an abstract handler mapping as follows:


abstract public class AbstractHandlerMapping implements HandlerMapping {
	public Object invoke(HandlerCtx hctx, HttpServletRequest rqst,
			HttpServletResponse rsp) throws Exception {
		Handler h = getHandler(hctx, rqst);
		if (h!=null) {
			Object r = h.invoke(hctx, rqst, rsp);
			return r;
		}
		return null;
	}

}

One possible concrete implementation for an HandlerMapping is to have a mapping for the first segment of a request path to a specific Handler. This is done below with class HandlerMap.


public class HandlerMap extends AbstractHandlerMapping {
	Map<String, Handler> map;

	public HandlerMap() {
		map = new HashMap<String, Handler>();
	}

	void addHandler(Handler h, String prefix) {
		map.put(prefix, h);
	}
	
	public Handler getHandler(HandlerCtx hctx, HttpServletRequest rqst) {
		String key = hctx.getPathSeg(false);
		if (key!=null) { return map.get(key); }
		return null;
	}
}

Another possible implementation is to select a Handler based on the longest matching prefix. (Left as an exercise).

To actually bind the Handler objects and containers to the DispatchServlet we use a root HandlerMapping that internally selects the Handler to dispatch request to. Method DispatchServlet.getHandler(..) can now have a very simple implementation.


class DispatcherServlet {
	Handler root;
	...	
	public Handler getHandler(HandlerCtx hctx, HttpServletRequest rqst)  {
		//ignore request path info (delegate to Handler or HandlerMapping)
		return getHandler(); 
	}
	
	Handler getHandler() {
		return root;
	}
	...
}

The request path is ignored in selecting a Handler, since the root Handler is always returned. This means that the DispatchServlet does not need to implement any Handler selection. It fully delegates this work to the root Handler, that is likely to be a HandlerMapping container. Method setHandler() is be used to set the root handler.

A convenient method addHandler() is also provided. It simply forwards the request to the root Handler if it is a HandlerMapping implementatoin. It also creates a root HandlerMapping automatically if it as not been set previously.


	void setHandler(Handler root) {
		this.root= root;
	}
	void addHandler(Handler h, String prefix) {
		if (root==null) root = new HandlerMap();
		if (root instanceof HandlerMap) ((HandlerMap)root).addHandler(h, prefix);
	}	

Model

You may notice that in the implementation above we left the model object to be of type Object instead of forcing the type Model. This gives more freedom to the application. Still, the type Model was introduced to be automatically created by MethodHandler and pass it to controller methods. A possible implementation is shown below:


public class Model {
	Map<String, Object> map;
	
	public Model() {
		map = new HashMap<String,Object>();
	}

	public Map<String, Object> getMap() { return map; }

	void addAttribute(Object value) {
		Class<?> ty = value.getClass();
		String name = genAttributeName(ty);
		addAttribute(name, value);
	}
	
	void addAttribute(String name, Object value) {
		map.put(name, value);
	}
	Object getAttribute(String name) {
		return map.get(name);
	}

	private String genAttributeName(Class<?> ty) {
		String s = ty.getName();
		return s.substring(0, 0).toLowerCase() + s.substring(1);
	}
}

Methods addAttribute() are used to register a named model variable. In one variant a variable name and value are passed as argument. The other variant with a single argument for the variable value uses a name that is automatically generated from the value type. This is done in method genAttributeName that produces the uncapitalized name of the variable type.

An additional type used in our MVC framework is the ModelView. This is simply a class with two fields (a tuple) --- one field for the model object and the other for the view. This allows controller methods to explicitly return a view object and a model object.


public class ModelView {
	Object model;
	View view;

	public ModelView(Object model, View view) {
		this.model = model;
		this.view = view;
	}
	
	//setters and getters follow
	...	
}

Views

A View is an object that actually render content to be sent to the client. Different types of View objects encapsulate different types of content such as a JSP, HTML, XML, PDF, or a plain text. To abstract all view content types, View is defined as an interface:


public interface View {
	void render(Object mdl, HttpServletRequest rqst,
		HttpServletResponse rsp, boolean include) throws Exception;
}

The single method render(..) takes as input a model object. This is the model object returned and populated by the application controller. It is used by dynamic views as input to evaluate expressions. The HttpServletResponse for the request is also passed in, since this is the object from where the output stream can be obtained. As explained above, the argument include is used to specify ig the view is the full content of the response or some fragment of it.

JSP Views

Java Server Pages (JSP) are a standard and commonly used technology to generate dynami web pages. Listing below shows class JspView, a View implementation that can be used to render JSP pages.


public class JspView implements View {
	String name;
	
	public JspView(String name) { this.name = name; }
	
	public void render(Object mdl, HttpServletRequest rqst,
			HttpServletResponse rsp, boolean include) throws Exception {
		RequestDispatcher rd = rqst.getRequestDispatcher(name);
		if (rd!=null) {
			loadModel(mdl, rqst, rsp);
			if (include) rd.include(rqst,rsp);
			else rd.forward(rqst,rsp);
		}
		else { 		
			throw new Exception("No such view:" + name);
		}
	}

	protected void loadModel(Object mdl, HttpServletRequest rqst,
			HttpServletResponse rsp) {
		if (mdl instanceof Model) {
			mdl = ((Model)mdl).getMap();
		}
		if (mdl instanceof Map<?, ?>) {
			Map<Object, Object> map = (Map<Object, Object>) mdl;
			for (Map.Entry<Object, Object> e: map.entrySet()) {
				Object key = e.getKey();
				Object value = e.getValue();
				rqst.setAttribute(key.toString(), value);
			}
		}
	}
}

The constructor argument for JspView is the resource to get the view content (filename). Method render() uses the standard Servlet API to get a RequestDispatcher object for the view resource. This is the object that can be used to display the view as an include or by forwarding the request.

Method loadModel(..) is used to set the rendering environment of the JSP page with the variables set in the model object. For each entry in the model object, a request scoped attribute is created. This are variables that have the life-time of the request and are shared between page includes and forward request. They are accessible from the JSP page with expression language syntax: ${varName} or, more generally, ${expr\.

Other Views

One can also implement other view types. Here we show the implementation of a simple TextView that outputs the text passed in the constructor directly to the response output stream. That is obtained with method getWriter() of the HttpServletResponse object.


public class TextView implements View {
	String text;
	
	public TextView(String text) { this.text = text; }
		
	public void render(Object mdl, HttpServletRequest rqst,
			HttpServletResponse rsp, boolean include) throws Exception {
		PrintWriter out = rsp.getWriter();
		out.print(text);
	}
}

Figure below summarizes the interfaces and class relationships in our MVC framework (in UML notation):

Interfaces and class relationship in the MVC framework.

Error Handling

At several points in the code show above we delegate error handling to a variety of send*Error() methods. Here, we show a minimal implementation of these methods. They simply output an error message. A more flexible implementation would allow the MVC framework to be configured with one or more error views.


public void sendExceptionError(Exception ex, HandlerCtx hctx,
	HttpServletRequest rqst, HttpServletResponse rsp)
	throws ServletException, IOException {
		PrintWriter out = rsp.getWriter();
	 	String path = rqst.getPathInfo();	
		out.printf("Exception while invoking:%s %s%n", hctx.getHttpMethod(), path);
	}
	
	public void sendMappingError(HandlerCtx hctx, HttpServletRequest rqst, HttpServletResponse rsp)
	throws ServletException, IOException {
		PrintWriter out = rsp.getWriter();
	 	String path = rqst.getPathInfo();	
	 	out.printf("No mapping for:%s %s%n", hctx.getHttpMethod(), path);
	}

	public void sendViewError(String viewFile, HttpServletRequest rqst, HttpServletResponse rsp)
	throws ServletException, IOException {
		PrintWriter out = rsp.getWriter();
	 	out.printf("<p>No such view::%s %n", viewFile);
	}


Testing and Deploying

To test the MVC framework we can implement a dummy application controller, as the one shown below:


public class MyController {
	public String f1() { return "/views/f1.jsp"; </b>
	public String f2() { return "f2"; </b>
	public void f3() { }	
}

If we deploy the DispatcherServlet and webapp as a standalone application with a embeded server (such as Jetty) then we can also hardcode the registration of the Handler objects. This could be done with one of the two approach shown below:


//enumerate methods explicitly (testing only)
static void register0(Object obj) {
	Object obj = new MyControler();
	
	try {
		dispatcher.addHandler(
			new MethodHandler(obj, obj.getClass().getMethod("f1")), "f1");
		dispatcher.addHandler(
			new MethodHandler(obj, obj.getClass().getMethod("f2")), "f2");
		dispatcher.addHandler(
			new MethodHandler(obj, obj.getClass().getMethod("f3")), "f3");
	} catch (NoSuchMethodException ex) { ex.printStackTrace(); }
}

//more realistic
static void register(Object obj) {
	Object obj = new MyControler();
	
	dispatcher.addHandler(new ClassHandler(obj), "/*");
}

If you are using an embeded server then your startup code may look something like the one shown below. Class WebServer is some implementation of a servlet container (e.g. extending Jetty Server class).


	public static void main(String[] args)  {
		WebServer server = new WebServer(8081);
		dispatcher = new DispatcherServlet();
		server.addServlet(dispatcher, "/mvc/*");

		register(new MyApp());
 
		String base = "/myhome/myapp";
		server.addResources(base);
	 
		try {
			System.out.println("Starting server....");
			server.start();
			server.join();
		} catch (Exception ex) { ex.printStackTrace(); }
	}	
}

On the other hand, if you are running the webapp and MVC framework as a WAR file deployed in a servlet container (such as TomCat) then the DispatcherServlet should be declared in the context descriptor file WEB-INF/web.xml. One also needs a way to configure the DispatcherServlet with the set of handlers or controllers of the application. This can be done in a variety of ways, such as: declaring controllers as servlet initialization parameters, using a dedicated XML configuration file, or by scanning annotation of application classes for controllers. Below, we show the simpler approach of specifying the controller class name in a servlet initialization parameter named controllers:


<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation=
	"http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
	version="2.4">

	<display-name>Testing MVC DispatcherServlet</display-name>
	<description>About MVC DispatcherServlet...
</description>

  <welcome-file-list>
	  <welcome-file>index.html</welcome-file>
  </welcome-file-list>

  <servlet>
		<servlet-name>DispatcherServlet</servlet-name>
		<servlet-class>mvc.DispatcherServlet</servlet-class>
		<description>MVC DispatcherServlet</description>
	 <init-param>
		<param-name>controllers</param-name>
		<param-value>mvc.MyApp</param-value>
	  </init-param>

	</servlet>

	<servlet-mapping>
		<servlet-name>DispatcherServlet</servlet-name>
		<url-pattern>/mvc/*</url-pattern>
	</servlet-mapping>
</web-app>

The code to actually get the intialization parameters, is setup in the init() method of the DispatcherServlet. The ServletConfig object is get with method getServletConfig(). Then servlet initialization parameters are get with method getInitParameterNames() and checked for the controllers parameter. An instance of the declared controller class is created, and a ClassHandler is added to the DispatcherServlet.


	public void init() {
		ServletConfig cfg = getServletConfig();
		java.util.Enumeration<String> pars =
			(java.util.Enumeration<String>) cfg.getInitParameterNames();
		while (pars.hasMoreElements()) {
			String name = pars.nextElement();
			Object val = cfg.getInitParameter(name);
			if (val==null) continue;
			try {
				if (name.equalsIgnoreCase("controllers")) {
					Class<?> cl = Class.forName(val.toString());
					Object obj = cl.newInstance();
					addHandler(new ClassHandler(obj), "/*");
				}
			} catch (Exception ex) {
				ex.printStackTrace();	
			}
		}		
	}

Summary and Conclusions

I have shown how to build a custom MVC framework with a DispatcherServlet working as front-controller. It relies on one or more Handler objects and types to dispatch the request to an application controller, and uses one or more View types to render the output to the client. Application controller methods may be injected with a set of values with recognized types. This includes the request parameter. Controller methods return a value that is interpreted as a view name, a model object, or both. Handler methods not returning a value (void return value) are automatically selected a logical viewname from the request path string. Logical view names are transformed into an actual resource file name. JspViews are implemented by using the standard RequestDispatcher objects, and by creating request scoped attributes to pass the value of model variable to the JSP page.

Exercises

If you want to make sure that you fully understand the topics presented in this article you may perform the exercises described below. When you are done it all of them you may feel confident that you have a full custom state-of-the art MVC framework to deploy your webapps.

  • Copy&Paste the code fragments presented in this article and setup a project to actually run and test the MVC framework. Deploy in you favorite servlet container.
  • In the presented implementation, a Model object is created by the MethodHandler if specified as argument to the controller method. However, the model is lost since it is not returned to the DispatcherServlet. This is a BUG! Fix it, by returning the model object from method MethodHandler.invoke() possibly wrapped in a ModelView object.
  • Define a new annotation @Name("name") for methods arguments. Modify MethodHandler to allow arguments of controller methods be annotated so that Http request methods can be mapped by name.
  • Modify or extend the ClassHandler to support method overloading.
  • Implement a Handler container class named HandlerMatcher that selects handler based on request prefix.
  • Modify the DispatcherServlet and MVC framework so that one or more view layout templates are automatically added before and after the content selected view. Check that the content of the template and content views match (i.e. a JSP template should be inserted only for JspView objects).
  • Read the documentation for the Jetty servlet container, and implement a Jetty Handler that invokes the DispatcherServlet. Run you test webapp and the MVC framework with an embedded Jetty server.
  • Read about Java features to process XML files, and implement your own XmlView class to render XML files. You may implement you own custom view technology by allowing interpreted expressions of the form ${expr} as attribute values of XML elements. Consider using the Java API for scripting languages if you don't feel like implementing your expression evaluation functions. Compare your view technology with JSP. Can you make it get better? How hard it to introduce something like JSP tag libraries?
  • Modify the the DispatcherServlet to read and process a XML configuration file, specifying the controller classes and request path prefixes for the web application.
  • Thinks about new features that you think may be useful in a MVC framework, and implement and test then in you custom MVC framework.
  • If you already have a fully-functional webapp (or close) implemented with several servlet or in a production ready MVC framework, try to port that application to you ready made custom MVC framework.
  • Think how security services could be integrated with the MVC framework.

External References


1 Comment

7/30/16

hi is there any source code github link of thi (How to Build a Custom Dispatcher Servlet and a MVC Framework) tutorial ...

Post Your Comment

Login (or Register)
Contribute Feedback