Spring MVC complex model population from multiple sources
Well, my question may sound a little bit fuzzy, but here it is, anyways. I'm building a web application using Spring MVC 3.1.M1, JSP 2.1 (without Tiles, I use plain JSP tag files to compose my layouts).
Basically, my pages are built using layouts of some common parts - header, footer, banner, menu etc. Most of these parts are dynamic, i.e. contain current user's related information.
JSP does not have a "component" notion, so I cannot define part of my template and its backing java code in some one place, coupled together. In my @Controllers, I have to fully populate my model, including data for header, footer, menu and other stuff. What I really want to do is to avoid this code duplication. Abstract BaseController class with some generic model population methods does not look good too.
JSP and Spring MVC are a very often used together, so I expect some best-practices to exist 开发者_Python百科on this subject. Lets discuss this.
Ok, so I spent some time with Spring MVC reference and sample applications, and found some additional ways to accomplish my mission. Here they are:
1) Way number one, bad and unusable, just to mention here. Abstract BaseController with methods like populateHeaderData(Model model), populateFooterData(Model model) and so on. All @RequestMapping methods in all controller classes that extend BaseController call these methods to populate layout-specific model data.
Pros: none
Cons: code duplication remains the same, just the amount of duplicated code is reduced
2) @ModelAttribute methods, i.e. implicit model enrichment. Looks like
@Controller
@RequestMapping(value="/account")
public class AccountController {
@ModelAttribute("visitorName")
private String putVisitor() {
return visitorService.getVisitorName();
}
// handler methods
}
And in JSP,
<span id="username">Welcome, ${visitorName}!</span>
Pros: no need to call model enrichment methods explicitly - it just works
Cons: its a tricky thing here. Spring MVC utilizes "push" templating model instead of "pull". What it means in this case is that when any of @RequestMapping methods, defined in this class, is called, all @ModelAttribute methods of this class are invoked. There is no difference if template really needs visitorName and if the template actually exist for specific action. This includes POST requests for form submits, etc. In fact, this forces us to change controllers separation. For example, all form submits should be in separate controller classes, and handler methods should be somehow grouped by layouts. I have to think more about it, maybe its not that bad as it looks at first glance.
More cons: suppose we have layouts A and B with the same non-static header, and B and C with the same non-static footer (all other parts are different). We cannot implement base class for layout B, since there is no multiple inheritance in Java.
Question to the audience: Spring MVC reference states "The following return types are supported for handler methods: A ModelAndView object, with the model implicitly enriched with command objects and the results of @ModelAttribute annotated reference data accessor methods... ". What the hell these command objects are?
3) My own pull-like method. We can create custom contexts in a form of
@Component("headerContext")
public class HeaderContext {
@Autowired
private VisitorService visitorService;
public String getVisitorName() {
return visitorService.getVisitorName();
}
// more getters here
}
Then, expose such beans to JSP EL via
<!-- Resolves view names to protected .jsp resources within the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/"/>
<beans:property name="suffix" value=".jsp"/>
<beans:property name="exposedContextBeanNames" value="headerContext,footerContext"/>
</beans:bean>
And in header.tag (JSP tag file for reused header)
<span id="username">Welcome, ${headerContext.visitorName}!</span>
Pros: "pull" strategy (nobody asks - nothing is exeduted), easy to make contexts @Scope("request") and enable request-wide caching, no problems with multiple inheritance. Just coded in one place, configured in one place and may be used in any JSP or tag file as a usual expression.
Cons: mix of push and pull within one framework (have to think more about it), no Spring MVC support in context implementation classes (I mean these nasty prepopulated arguments in controller handler methods), just spring beans.
The springframework contains handler interceptors as part of the handler mapping mechanism.
Within the interceptor you can use the postHandle
method before the actual handler is executed.
Such a interceptor must implement the org.springframework.web.servlet.HandlerInterceptor
or the org.springframework.web.servlet.handler.HandlerInterceptorAdapter
for simplified implementation.
public class MyHandlerInterceptor extends HandlerInterceptorAdapter {
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {
//populate header, menu, footer, ... model
}
}
and the configuration for the handler mapping.
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name="interceptors">
<list>
<bean id="myInterceptor" class="...MyHandlerInterceptor"/>
</list>
</property>
Finally, I decided to stick with @ModelAttribute approach, despite its limitations.
/**
* Base class for all page controllers (i.e., not form submits)
* @author malexejev
* 23.03.2011
*/
public abstract class AbstractPageController {
@Autowired
private VisitorService visitorService;
@Autowired
private I18nSupport i18nSupport;
@Value("${xxx.env}")
private String environment;
/**
* Implicit model enrichment with reference data.
* No heavy operations allowed here, since it is executed before any handler method of
* all extending controllers
*/
@ModelAttribute("appContext")
public Map<String, Object> populateReferenceData(HttpServletRequest request) {
Map<String, Object> dataMap = new HashMap<String, Object>();
// FIXME some data is app-wide and constant, no need to re-create such map entries
// I should take care about it when more reference data is added
dataMap.put("visitorName", visitorService.getVisitorName());
dataMap.put("env", environment);
dataMap.put("availableLanguages", i18nSupport.getAvailableLanguages());
dataMap.put("currentPath", request.getPathInfo() != null ? request.getPathInfo() : request.getServletPath());
return Collections.unmodifiableMap(dataMap);
}
}
This way i can get data in views via ${appContext.visitorName}. It allows me to switch transparently to Spring bean implementation (see No 3 in my answer above, @Component("headerContext") ) in case of any future problems with @ModelAttributes.
Thanks all to discussion. I dont see any "silver bullet" solution found here, so I will not mark any answer as accepted, but will vote up all answers to this question.
well you have several options, though they are not perfect either..
- abstract controller like you mentioned
- create a service that will return you the model data.. now youve moved the problem into the service layer where it arguably doesnt belong, but at least your controllers can just make one service call during each controller method.
- create a filter and populate the common parts of the model in the filter.
- you can probably create some monster with annotations, for example, annotate the controller methods and then post processes the controller objects to inject the data in (this, i am not sure how to do exactly, but there must be a way)
- spring AOP might be able to help you do #4 more gracefully
these are just some ideas to get some discussion going
handler interceptor is fine for shared data which is used in every page.
if you want fine grained "components" you really should reconsider using apache tiles. from there you can use a "controller" (ViewPreparer) for each tile as pointed here:
http://richardbarabe.wordpress.com/2009/02/19/apache-tiles-2-viewpreparer-example/
精彩评论