论坛首页 Java企业应用论坛

C/S请求与B/S请求统一MVC控制器

浏览 2176 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2008-11-12   最后修改:2008-12-03
J2EE中基于B/S的MVC框架不少,设计思想也在不停的进步,从Struts1.x开始,每个人都站在巨人的肩膀上,一步一个脚印。
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());

在泛型的支持下,客户端与服务器端都不需要做任何强制转型,语义更明确。
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics