浏览 2176 次
锁定老帖子 主题:C/S请求与B/S请求统一MVC控制器
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2008-11-12
最后修改:2008-12-03
MVC框架主要以控制器为中心,简化模型与视图的交互过程, 控制器要做哪些事? 1. 页面流控制,也就是一级级forward的处理。 2. 接收数据,从表单或URL上传来的数据,需要透明化接收(即:不能让业务逻辑看到接收过程)。 3. 呈现数据,将数据传到页面,或下一forward控制器。 其它的拦截器,前端校验,模板回调,标签库封装等都是附属功能,围绕控制器转。 以业务实现者的角度看, 控制器接口需要有一个传入参数表示接收到的数据, 有两个传出参数,一个表示呈现数据,一个表示页面跳转索引。 这刚好和Java函数的设计相反,函数不允许返回多个参数,怎么办呢? 因为这样,使得控制器接口的设计,变得百家争鸣,各不相同。 Struts(1.x)给出的方案: public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // ...... } 它将返回值作为页面流控制,数据的传入传出都用ActionForm,当然用户也可以直接往request里set。 这个设计是比较差的: 1. ActionForward的包装过程应该由框架做,而不是由用户去ActionMapping中查找,用户应该只需返回最简单的String索引号。 2. ActionMapping不应在接口暴露,它不是业务逻辑所关注的,可以将这些附属环境信息使用ThreadLocal锁定在上下文中,如:ActionContext.getContext().getActionMapping(); 3. HttpServletRequest, HttpServletResponse不应对用户公开,因为接口已经拥有ActionForm作为数据模型接口,公开Request和Response,只会误导用户绕过框架,也致使框架严重依赖Servlet容器,不便于单元测试,Mock容器是非常痛苦的事情。 4. ActionForm应该为任意POJO,最好用Serializable作为接口声明,使用户可以将实体模型作为表单模型使用。 综上四点可改成: public String execute(Serializable form) throws Exception { // ...... } 再加上Action上下文: public class ActionContext { public static ActionContext getContext() {...} public ActionMapping getActionMapping(){...} public HttpServletRequest getRequest(){...} public HttpServletResponse getResponse(){...} } struts1.x出身早,也怪不得。 我们再来看SpringMVC的改进: Rod Johnson估计是对Struts1.x早就看不惯了,在IoC/DI容器里,搞起了重复发明轮子的事。 public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { // ...... } SpringMVC最直接了当了,你不是需要返回两个值吗?给你包装一下就是了,ModelAndView,呈现数据模型与页面控制的封装体。 但这里同样存在与Servlet容器高度耦合,测试极不方便,也没做到数据模型透明化传递。 当然,SpringMVC提供模板方法适配,以做到透明化数据接收,和隔离Servlet容器的作用,如SimpleFormController: public ModelAndView onSubmit(Object command, BindException errors) throws Exception { // ...... } 而WebWork(包括Struts2.0)的设计稍微合理一些: public String execute() throws Exception { // ...... } public void setXXX() { // 接收数据 } public void getYYY() { // 呈现数据 } 接口函数的返回值用于页面跳转,传入数据使用setter属性,传出数据使用getter属性, 这个设计是出乎意料的,在其它框架的Action都是单实例线程安全的情况下,它却选择了非单实例线程安全的Action控制器接口, 也就是每一次请求都需要创建新的实例,每个实例只为一次请求服务, 这一选择,使得它不再为函数签名发愁,因为它可以动用整个控制器实例的所有函数, 也使得控制器演变成了命令模式,Action封装了整个执行体,而不是一个服务。 前些天写了一个适用于RCP/RIA应用的MVC框架: http://javatar.iteye.com/blog/264509 虽然没发布,但也有个雏形了。 这两天在思考与C/S请求与B/S请求统一控制器的问题,也就想了上面所写的一通, 如果struts4rcp也接收传统B/S页面请求,应该怎么设计控制器接口? 不太想采用WebWork的向控制器注入数据的方式,希望将模型与服务域分离。 晚上,终于想到一个比较好的接口设计方案。 接口设计为什么不能动用异常?不就有两个返回值了?如: public Serializable execute(Serializable model) throws ForwardException { // ...... } 跳转异常信息类如下: /** * 请求跳转异常 * @author <a href="mailto:liangfei0201@gmail.com">liangfei</a> */ public class ForwardException extends RuntimeException { private static final long serialVersionUID = -2512411385294660606L; private final String actionName; private final Serializable model; /** * @param actionName 跳转Action名称 * @param model 跳转传递数据模型 */ public ForwardException(String actionName, Serializable model) { this.actionName = actionName; this.model = model; } public String getActionName() { return actionName; } public Serializable getModel() { return model; } @Override public String toString() { return "forward to:" + actionName; } } 用返回值作数据模型,用异常作页面跳转,当该问search.action,却被跳传到了login.action,这算不算一种异常情况? 按照REST的说法,每个URL代表一个资源,一个URL得到了两种结果算不算异常? 实际项目中,真正在控制器中跳转的非常少,并且通常是success和failure, 而failure,完全可以作为一个异常处理。 另外,可以加入泛型,使接口契约更强, 优化后的接口如下: /** * 数据传输Action接口 * @author <a href="mailto:liangfei0201@gmail.com">liangfei</a> * @param <M> 传入模型类型 * @param <R> 返回值类型 */ public interface Action<M extends Serializable, R extends Serializable> { /** * 执行Action * @param model 传入参数 * @return 传回返回值 * @throws Exception 异常均向上抛出,由框架统一处理,包括: ForwardException */ R execute(M model) throws Exception; } 请求类型的三种情况: 1. B/S页面请求: 传入参数表示表单(或URL)数据,由框架自动注入到POJO属性中,返回值作为呈现模型,可以在JSP中直接使用。 2. B/S AJAX请求: 通过AJAX请求传入一个JSON串,由框架格式化为一个POJO,返回值同样由框架反格式化为JSON串,客户端JavaScript可以将JSON看成对象的对等形式。 3. C/S请求:客户端直接拿到接口代理进行执行,如:Action<User, Account> action = Actions.getAction("loginAction"); User user = action.execute(account); 以登录为例: /** * 登录控制器 * @author <a href="mailto:liangfei0201@gmail.com">liangfei</a> */ public class LoginAction implements Action<Account, User> { private LoginService loginService; // IoC注入 public void setLoginService(LoginService loginService) { this.loginService = loginService; } public User execute(Account account) throws Exception { // 如果需要跳转,可以用:throw new ForwardException("xxxAction", account); return loginService.login(account.getUsername(), account.getPassword()); } } /** * 帐号信息 * @author <a href="mailto:liangfei0201@gmail.com">liangfei</a> */ public class Account implements Serializable { private static final long serialVersionUID = 1L; private String username; private String password; public Account() {} public Account(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } /** * 用户实体信息 * @author <a href="mailto:liangfei0201@gmail.com">liangfei</a> */ public class User implements Serializable { private static final long serialVersionUID = 4544208653256289206L; private Long id; private String username; private String password; private String email; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } Spring配置: <bean id="loginAction" class="com.xxx.LoginAction"> <property name="loginService" ref="loginService" /> </bean> 下面我们分别来看三种请求的调用方式: 1. B/S页面请求: <form action="loginAction" method="post"> <input type="text" name="username" /> <input type="password" name="password" /> <!--类元信息,若没有class属性,服务器端需用Map接收--> <input type="hidden" name="class" value="com.xxx.Account" /> </form> 响应页面:(可直接使用User对象的属性) 您好,${username},您的邮箱是:${email} 2. B/S AJAX请求: var account = {username: "james", password: "123456", class: "com.xxx.Account"}; // 传入JSON数据模型,若没有class属性,服务器端需用Map接收 var loginAction = Actions.getAction("loginAction"); var user = loginAction.execute(account); // 执行,并得到JSON结果 alert(user.email); 3. C/S请求: Account account = new Account("james", "123456"); // 传入数据模型 Action<Account, User> loginAction = Actions.getAction("loginAction"); // 看起像拿到了服务器端Action的引用(即透明化) User user = loginAction.execute(account); // 执行,并得到User对象 System.out.println(user.getEmail()); 在泛型的支持下,客户端与服务器端都不需要做任何强制转型,语义更明确。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |