来自多个来源的 Spring MVC 复杂模型群体

发布于 2024-10-25 03:46:52 字数 421 浏览 1 评论 0原文

好吧,我的问题可能听起来有点模糊,但无论如何,它就是这样。 我正在使用 Spring MVC 3.1.M1、JSP 2.1 构建一个 Web 应用程序(没有 Tiles,我使用纯 JSP 标记文件来组成我的布局)。

基本上,我的页面是使用一些常见部分的布局构建的 - 页眉、页脚、横幅、菜单等。这些部分大多数是动态的,即包含当前用户的相关信息。

JSP 没有“组件”概念,因此我无法在某个地方定义部分模板及其支持的 java 代码,并将其耦合在一起。在我的 @Controllers 中,我必须完全填充我的模型,包括页眉、页脚、菜单和其他内容的数据。我真正想做的是避免这种代码重复。具有一些通用模型填充方法的抽象 BaseController 类看起来也不太好。

JSP 和 Spring MVC 经常一起使用,因此我希望在这个主题上存在一些最佳实践。 我们来讨论一下这个。

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 on this subject.
Lets discuss this.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

鯉魚旗 2024-11-01 03:46:52

好的,我花了一些时间研究 Spring MVC 参考和示例应用程序,并找到了一些其他方法来完成我的任务。它们是:

1) 第一种方式,糟糕且无法使用,仅在此提及。抽象 BaseController,具有 populateHeaderData(Model model)、populateFooterData(Model model) 等方法。扩展 BaseController 的所有控制器类中的所有 @RequestMapping 方法都会调用这些方法来填充特定于布局的模型数据。

优点:

缺点: 代码重复保持不变,只是重复代码量减少了

2) @ModelAttribute 方法,即隐式模型丰富。看起来像

@Controller
@RequestMapping(value="/account")
public class AccountController {

    @ModelAttribute("visitorName")
    private String putVisitor() {
        return visitorService.getVisitorName();
    }

    // handler methods
}

在 JSP 中,

<span id="username">Welcome, ${visitorName}!</span>

优点:不需要显式调用模型丰富方法 - 它只是工作

缺点:这是一件棘手的事情。 Spring MVC 使用“推”模板模型而不是“拉”。在这种情况下,这意味着当调用此类中定义的任何 @RequestMapping 方法时,将调用此类的所有 @ModelAttribute 方法。如果模板确实需要访问者名称以及模板是否确实存在用于特定操作,则没有区别。这包括表单提交的 POST 请求等。事实上,这迫使我们改变控制器分离。例如,所有表单提交都应该位于单独的控制器类中,并且处理程序方法应该以某种方式按布局分组。我得再想一想,也许事情并没有乍看起来那么糟糕。

更多缺点:假设我们的布局 A 和 B 具有相同的非静态页眉,B 和 C 具有相同的非静态页脚(所有其他部分都不同)。我们无法实现布局 B 的基类,因为 Java 中没有多重继承。

向观众提问:
Spring MVC 参考指出“处理程序方法支持以下返回类型:ModelAndView 对象,其中模型隐式地通过命令对象和 @ModelAttribute 注释的参考数据访问器方法的结果进行丰富......”。这些命令对象到底是什么?

3) 我自己的类似拉动的方法。我们可以以 Then 的形式创建自定义上下文

@Component("headerContext")
public class HeaderContext {

    @Autowired
    private VisitorService visitorService;

    public String getVisitorName() {
        return visitorService.getVisitorName();
    }

    // more getters here

}

将这些 bean 公开给 JSP EL

<!-- 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 标记文件)

<span id="username">Welcome, ${headerContext.visitorName}!</span>

优点: “拉”策略(没有人问 - 没有什么execeded),轻松创建上下文@Scope(“request”)并启用请求范围的缓存,多重继承没有问题。只需在一处编码、在一处配置,并且可以作为常用表达式在任何 JSP 或标记文件中使用。

缺点:在一个框架内混合推和拉(必须更多地考虑它),上下文实现类中没有 Spring MVC 支持(我的意思是控制器处理程序方法中这些令人讨厌的预填充参数),只有 spring bean 。

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.

所谓喜欢 2024-11-01 03:46:52

springframework 包含处理程序拦截器作为处理程序映射机制的一部分。
在拦截器中,您可以在执行实际处理程序之前使用 postHandle 方法。

这样的拦截器必须实现 org.springframework.web.servlet.HandlerInterceptor 或 org.springframework.web.servlet.handler.HandlerInterceptorAdapter 以简化实现。

public class MyHandlerInterceptor extends HandlerInterceptorAdapter {

    public void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {

        //populate header, menu, footer, ... model
    }
}

以及处理程序映射的配置。

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name="interceptors">
    <list>
        <bean id="myInterceptor" class="...MyHandlerInterceptor"/>
    </list>
</property>

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>
開玄 2024-11-01 03:46:52

最后,我决定坚持使用 @ModelAttribute 方法,尽管它有其局限性。

/**
 * 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);
    }

}

这样我就可以通过 ${appContext.visitorName} 获取视图中的数据。它允许我透明地切换到 Spring bean 实现(请参阅上面我的答案中的第 3 个 @Component("headerContext") ),以防将来出现 @ModelAttributes 的任何问题。

谢谢大家讨论。我在这里没有看到任何“银弹”解决方案,因此我不会将任何答案标记为已接受,但会投票赞成该问题的所有答案。

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.

温柔戏命师 2024-11-01 03:46:52

好吧,你有几个选择,尽管它们也不完美..

  1. 像你提到的抽象控制器
  2. 创建一个将返回模型数据的服务..现在你已经将问题转移到服务层,它可以说不属于它,但至少你的控制器在每个控制器方法期间只能进行一次服务调用。
  3. 创建一个过滤器并在过滤器中填充模型的公共部分。
  4. 你可能可以用注释创建一些怪物,例如,注释控制器方法,然后对控制器对象进行后期处理以将数据注入(这个,我不知道具体该怎么做,但一定有办法)
  5. spring AOP 可能能够帮助你更优雅地完成#4

这些只是一些让讨论继续下去的想法

well you have several options, though they are not perfect either..

  1. abstract controller like you mentioned
  2. 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.
  3. create a filter and populate the common parts of the model in the filter.
  4. 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)
  5. spring AOP might be able to help you do #4 more gracefully

these are just some ideas to get some discussion going

刘备忘录 2024-11-01 03:46:52

处理程序拦截器适用于每个页面中使用的共享数据。

如果你想要细粒度的“组件”,你真的应该重新考虑使用 apachetiles。
从那里您可以使用“控制器”(ViewPreparer )对于此处指出的每个图块:

http:// richardbarabe.wordpress.com/2009/02/19/apache-tiles-2-viewpreparer-example/

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/

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文