Introduction:
In many projects, developers implement their own MVC framework, not because they want to do something fundamentally different from Struts, but because they are not aware of how to expand Struts. Developing Your Own MVC framework can gain full control, but it also requires a lot of resources to implement it (human and material resources). Sometimes this is impossible under a tight schedule.
Struts is not only a powerful framework, but also scalable. You can expand Struts in three ways.
1. PlugIn: If you want to do some business logic during application startup or shutdown, create your own PlugIn class.
2. RequestProcessor: If you want to make some business logic at a certain time point in the request processing process, create your own RequestProcessor class. For example, before each request is executed, you can extend RequestProcessor to check whether the user has logged in and whether he has the permission to execute a specific action.
3. ActionServlet: If you want to perform some business logic during application startup and shutdown and when the request is processed, you can also expand the ActionServlet class. However, you should use ActionServlet when PlugIn and RequestProcessor cannot solve your needs.
In this article, we will use a Struts application example to demonstrate how to use these three methods to expand Struts. The sample code can be downloaded from http://www.onjava.com/onjava/2004/11/10/examples/sample1.zip. Two successful examples of Struts extension are Struts's Validation and Tiles framework.
Let's assume that you are familiar with the Struts framework and know how to use it to create a simple application. For more information about Struts, see the official homepage.
PlugIn
PlugIn is an interface. You can create a class to implement this interface and do something when application startup or shutdown.
For example, I have created a web application that uses Hibernate as the persistent layer. I want to initialize Hibernate when the application is started, so that when my web application receives the first request, hibernate is already configured and available. At the same time, we want to disable Hibernate when the application is closed. We can use a Hibernate PlugIn to implement this requirement. The following two steps are taken:
1. Create a class to implement the PlugIn Interface:
Public class HibernatePlugIn implements PlugIn {
Private String configFile;
// This method will be called at application shutdown time
Public void destroy (){
System. out. println ("Entering HibernatePlugIn. destroy ()");
// Put hibernate cleanup code here
System. out. println ("Exiting HibernatePlugIn. destroy ()");
}
// This method will be called at application startup time
Public void init (ActionServlet actionServlet, ModuleConfig config)
Throws ServletException {
System. out. println ("Entering HibernatePlugIn. init ()");
System. out. println ("value of init parameter" +
GetConfigFile ());
System. out. println ("Exiting HibernatePlugIn. init ()");
}
Public String getConfigFile (){
Return name;
}
Public void setConfigFile (String string ){
ConfigFile = string;
}
}
The class that implements the PlugIn interface must complete two methods: init () and destroy (). The init () method is called when the application is started, and the destroy () method is called when shutdown. Struts also allows you to pass initialization parameters to your PlugIn class. To pass parameters, you must create a setan setter method for each parameter in the PlugIn class. In our HibernatePlugIn class, I will pass the name of configFile as a parameter, rather than hardcoding it into the program.
2. Add the following code to the struts-config.xml to notify Struts of a new PlugIn:
<Struts-config>
...
<! -- Message Resources -->
<Message-resources parameter = "sample1.resources. ApplicationResources"/>
<! -- Declare your plugins -->
<Plug-in className = "com. sample. util. HibernatePlugIn">
<Set-property = "configFile" value = "/hibernate. cfg. xml"/>
</Plug-in>
</Struts-config>
The attribute className is the fully qualified name of the class that implements the PlugIn interface. For each initialization parameter, you can use the <set-property> element to pass the parameter. In our example, I want to pass the config file name, so I used a <set-property> with the configuration file path.
Struts's Tiles and Validator frameworks both use PlugIn to read the configuration file for initialization. The other two plug-ins can help you do the following:
· If your application depends on some configuration files, you can check whether they are available in the PlugIn class. If they are unavailable, a ServletException will be thrown, which will make the ActionServlet unavailable.
· The init () method of the PlugIn interface is the last opportunity for you to change ModuleConfig. ModuleConfig is a set of static configuration information to describe the Struts-based module. Struts releases ModuleConfig after all plugins are processed.
How the Request is processed
ActionServlet is the only Servlet in the Struts framework and is responsible for processing all requests. Whenever a request is received, it first tries to find a sub-application for the current request. Once a sub-application is found, ActionServlet creates a RequestProcessor object for that sub-application, calls the process () method of this object, and passes in the HttpServletRequest and HttpServletResponse objects.
RequestProcessor. process () is where most requests are processed. The process () Method uses the Template Method mode. Many independent methods are used to execute each step of request processing. These methods are called in sequence in the process Method. For example, there will be an independent method to find the ActionForm class corresponding to the current request, and a method to check whether the current user has the required permissions to execute action mapping. These give us great flexibility. A RequestProcessor class in the published Struts package provides the default implementation for each step of request processing. This means that you can simply rewrite the method you are interested in, and use the default implementation for others. For example, by default, Struts calls request. isUserInRole () to check whether the user has the permission to execute the current ActionMapping. If you want to query the database, all you need to do is override the processRoles () method, returns true or false if the queried user has the required permissions.
First, we will see how the process () method is implemented by default, and then I will explain in detail every method in the default RequestProcessor class, in this way, you can decide which part you want to change.
Public void process (HttpServletRequest request, HttpServletResponse response)
Throws IOException, ServletException {
// Wrap multipart requests with a special wrapper
Request = processMultipart (request );
// Identify the path component we will
// Use to select a mapping
String path = processPath (request, response );
If (path = null ){
Return;
}
If (log. isDebugEnabled ()){
Log. debug ("Processing a'" + request. getMethod () + "'for path'" + path + "'");
}
// Select a Locale for the current user if requested
ProcessLocale (request, response );
// Set the content type and no-caching headers
// If requested
ProcessContent (request, response );
ProcessNoCache (request, response );
// General purpose preprocessing hook
If (! ProcessPreprocess (request, response )){
Return;
}
// Identify the mapping for this request
ActionMapping mapping =
ProcessMapping (request, response, path );
If (mapping = null ){
Return;
}
// Check for any role required to perform this action
If (! ProcessRoles (request, response, mapping )){
Return;
}
// Process any ActionForm bean related to this request
ActionForm form = processActionForm (request, response, mapping );
ProcessPopulate (request, response, form, mapping );
If (! ProcessValidate (request, response, form, mapping )){
Return;
}
// Process a forward or include specified by this mapping
If (! ProcessForward (request, response, mapping )){
Return;
}
If (! ProcessInclude (request, response, mapping )){
Return;
}
// Create or acquire the Action instance
// Process this request
Action action =
ProcessActionCreate (request, response, mapping );
If (action = null ){
Return;
}
// Call the Action instance itself
ActionForward forward = processActionPerform (request, response, action, form, mapping );
// Process the returned ActionForward instance
ProcessForwardConfig (request, response, forward );
}
1. processMutipart (): In this method, Struts will read the request to check whether the contentType of the request is multipart/form-data. If yes, the request will be parsed and packaged into HttpServletRequest. When you create an html form to submit data, the contentType of the request is application/x-www-form-urlencoded by default. However, if your form uses the file-type input control to allow users to upload files, you must change contentType to multipart/form-data. In this case, you cannot use getParameter () to obtain the data submitted by the user. You must read the request as an InputStream and parse it to obtain the parameter value.
2. processPath (): In this method, Struts will read the URI of the request to determine the path element. This element is used to obtain the ActionMappint element.
3. processLocale (): In this method, Struts will obtain Locale for the current request. If configured, it can also use this object as org. apache. struts. action. save the value of the LOCALE attribute. As a side effect of this method, HttpSession will be created. If you don't want to create it, you can set the locale attribute to false in ControllerConfig, as shown below in the struts-config.xml:
<Controller>
<Set-property = "locale" value = "false"/>
</Controller>
4. processContent (): Call response. setContentType () to set contentType for response. This method first tries to get contentType from the struts-config.xml configuration. Text/html is used by default. To overwrite it, you can do the following:
<Controller>
<Set-property = "contentType" value = "text/plain"/>
</Controller>
5. processNoCache (): If no-cache is configured, Struts will set the following three headers for each response:
Requested in struts config. xml
Response. setHeader ("Pragma", "No-cache ");
Response. setHeader ("Cache-Control", "no-cache ");
Response. setDateHeader ("Expires", 1 );
If you want to set the no-cache header, add the following information to the struts-config.xml:
<Controller>
<Set-property = "noCache" value = "true"/>
</Controller>
6. processPreprocess (): This method provides a hook for preprocessing and can be overwritten in the subclass. Its default implementation always returns true if nothing is done. If false is returned, the processing of the current request will be terminated.
7. processMapping (): This method uses the path information to obtain an ActionMapping object. That is, the <action> element in the struts-config.xml file:
<Action path = "/newcontact" type = "com. sample. NewContactAction" name = "newContactForm" scope = "request">
<Forward name = "sucess" path = "/sucessPage. do"/>
<Forward name = "failure" path = "/failurePage. do"/>
</Action>
The ActionMapping element contains the name of the Action class and the ActionForm used to process the request. It also contains the ActionForwards information of the current ActionMapping configuration.
8. processRoles (): the Struts web application provides an authorization solution. That is to say, once a user logs on to the container, the struts processRoles () method will check whether a user has a required role to run a given ActionMapping by calling request. isUserInRole.
<Action path = "/addUser" roles = "administrator"/>
Suppose you have an AddUserAction and you only want the administrator to add a new user. All you need to do is add a role attribute to your AddUserAction element. The value of this attribute is administrator. In this way, before running AddUserAction, this method ensures that the user has the role of administraotr.
9. processActionForm (): Each ActionMapping has an ActionForm class. When Struts processes an ActionMapping, it will find the corresponding ActionForm class name from the name attribute of the <action> element.
<Form-bean name = "newContactForm" type = "org. apache. struts. action. DynaActionForm">
<Form-property name = "firstName" type = "java. lang. String"/>
<Form-property name = "lastName" type = "java. lang. String"/>
</Form-bean>
In our example, it will first check whether there is an org. apache. struts. action. DynaActionForm class object in the request scope. If it exists, it will use this object. If it does not exist, it will create a new object and set it in the request scope.
10. processPopulate (): In this method, Struts will assemble the instance variables of ActionForm with the matching request parameters.
11. processValidate (): Struts will call the validate method of your ActionForm class. If you return ActionErrors from validate (), It redirects the user to the page specified by the input attribute of the <action> element.
12. processForward () and processInclude (): In these methods, Struts checks the forward or include attribute of the <action> element, the forward or include requests are placed on the configuration page.
<Action forward = "/Login. jsp" path = "/loginInput"/>
<Action include = "/Login. jsp" path = "/loginInput"/>
You can guess their differences from the names of these methods: processForward () finally calls RequestDispatcher. forward (), while processInclude () calls RequestDispatcher. include (). If both the forward and include attributes are configured, forward is always called because forward is processed first.
13. processActionCreate (): This method obtains the name of the action class from the type attribute of the <Action> element and creates an instance that returns it. In our example, it will create an instance of the com. sample. NewContactAction class.
14. processActionPerform (): This method calls the excute () method of your Action class, and your business logic is in the excute method.
15. processForwardConfig (): The excute () method of your Action class will return an ActionForward object, which will indicate which page is displayed to the user. Therefore, Struts will create a RequestDispatcher for that page and call RequestDispatcher. forward ().
The list above illustrates the work and execution sequence of the default RequestProcessor in each step of request processing. As you can see, RequestProcessor is very flexible, allowing you to configure it by setting the attribute of the <controller> element. For example, if your application is preparing to generate XML content instead of HTML, you can set the attributes of the controller element to notify Struts of such situations.
Create your own RequestProcessor
Through the above, we learned how the default Implementation of RequestProcessor works. Now we will demonstrate an example to illustrate how to customize your own RequestProcessor. To demonstrate how to create a custom RequestProcessor, we will let our example implement the following two business needs:
· We want to create a ContactImageAction class that will generate images instead of common HTML pages.
· Before processing each request, we want to check the userName attribute in the session to determine whether the user has logged on. If the property is not found, we will redirect the user to the login page.
We will implement these business needs in two steps.
1. Create Your CustomRequestProcessor class, which will inherit from the RequestProcessor class, as shown below:
Public class CustomRequestProcessor
Extends RequestProcessor {
Protected boolean processPreprocess (
HttpServletRequest request, HttpServletResponse response ){
HttpSession session = request. getSession (false );
// If user is trying to access login page
// Then don't check
If (request. getServletPath (). equals ("/loginInput. do ")
| Request. getServletPath (). equals ("/login. do "))
Return true;
// Check if userName attribute is there is session.
// If so, it means user has allready logged in
If (session! = Null & session. getAttribute ("userName ")! = Null)
Return true;
Else {
Try {
// If no redirect user to login Page
Request. getRequestDispatcher ("/Login. jsp"). forward (request, response );
} Catch (Exception ex ){
}
}
Return false;
}
Protected void processContent (HttpServletRequest request,
HttpServletResponse response ){
// Check if user is requesting ContactImageAction
// If yes then set image/gif as content type
If (request. getServletPath (). equals ("/contactimage. do ")){
Response. setContentType ("image/gif ");
Return;
}
Super. processContent (request, response );
}
}
In the processPreprocess method of the CustomRequestProcessor class, we check the userName attribute of the session. If not found, the user will be redirected to the login page.
To generate an image as an output, we must overwrite the processContent method. First, check whether the request is a/contactimage path. If yes, we will set contentType to image/gif; otherwise, it will be set to text/html.
2. Add the following text after the <action-mappint> element of your struts-config.xml file to tell Struts CustomRequestProcessor to be used as the RequestProcessor class:
<Controller>
<Set-property = "processorClass" value = "com. sample. util. CustomRequestProcessor"/>
</Controller>
Note that when only a few action classes need to generate non-text/html output, the processContent () method is OK. If this is not the case, you should create a Struts sub-application to process the request to generate the image Action, and set contentType to image/gif for them.
Struts's Tiles framework uses its own RequestProcessor to describe Struts output.
ActionServlet
If you view the web. xml of your Struts web application, you will see the following text:
<Web-app>
<Servlet>
<Servlet-name> action = </servlet-name>
<Servlet-class> org. apache. struts. action. ActionServlet </servlet-class>
<! -- All your init-params go here -->
</Servlet>
<Servlet-mapping>
<Servlet-name> action </servlet-name>
<Url-pattern> *. do </url-pattern>
</Servlet-mapping>
</Web-app>
This means that ActionServlet is responsible for processing all your Struts requests. You can create an ActionServlet subclass. When an application is started or closed, specific tasks are performed for each request. However, before inheriting the ActionServlet class, you should try to create a PlugIn or RequestProcessor to solve your problem. Before Servlet1.1, the Tiles framework modifies the generated response based on ActionServlet. However, since 1.1, it began to use the TilesRequestProcessor class.
Summary
Deciding to develop your own MVC Framework is a big decision. You must consider the time and resources required to develop and maintain the Framework Code. Struts is a very powerful and stable framework. You can modify it to meet most of your business needs.
On the other hand, do not rashly make the decision to expand Struts. If you write some low-performance code in RequestProcessor, it will execute each request, thus reducing the efficiency of your entire application. In some cases, developing Your Own MVC framework is better than extending Struts.