上一篇中通过前端控制器实现了接收请求. 控制器在收到请求后进行业务逻辑处理, 需要将视图返回至前端控制器, 由前端控制器并将结果返回至客户端.
常见的返回结果有以下两种:
前端控制器中需要接收控制器的返回结果, 在返回结果中约定: 如果以"redirect:"开始, 则为返回结果是重定向, 反之为视图, 前端控制器渲染视图后返回客户端.
private void doService(HttpServletRequest req, HttpServletResponse resp) throws Exception { // 获取对应的方法 // 参数绑定 // ... // 执行控制器方法并接收返回值 String view = (String) method.invoke(classInstance, methodParams); // 根据返回值判断重定向或渲染视图(JSP) if (view.startsWith("redirect:")) { resp.sendRedirect(view.substring(9)); } else { // 将控制器中保持的变量设置到Request中 for (Map.Entry<String, Object> entry : model.entrySet()) { req.setAttribute(entry.getKey(), entry.getValue()); } // 跳转至相应的JSP中 req.getRequestDispatcher(view + ".jsp").forward(req, resp); } } 复制代码
随着模板的兴起, 现在有越来越多的模板技术取代JSP成为视图. 作为负责渲染视图的前端控制器也应该支持多种视图方式供用户选择.
在前端控制器中定义视图类型, 并在初始化时加载.
// 视图类型(jsp,freemarker,velocity...) private String viewType = "jsp"; /** * 初始化Servlet. 容器初始化Servlet时调用, 加载配置文件初始化MVC相关组件(控制器,视图解析器等) */ @Override public void init() throws ServletException { // 获取用户自定义的视图类型 String viewTypeConfig = getInitParameter("viewType"); if (viewTypeConfig != null) { this.viewType = viewTypeConfig; } } 复制代码
在 web.xml
中配置视图类型
<servlet> <servlet-name>mvc</servlet-name> <servlet-class>com.atd681.xc.ssm.framework.DispatcherServlet</servlet-class> <!-- 视图类型 --> <init-param> <param-name>viewType</param-name> <param-value>freemarker</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> 复制代码
在解析视图时, 根据配置的视图类型使用对应的方式解析视图
private void doService(HttpServletRequest req, HttpServletResponse resp) throws Exception { // 返回的视图 String view = (String) method.invoke(classInstance, methodParams); // 根据返回值判断重定向或渲染视图 if (view.startsWith("redirect:")) { resp.sendRedirect(view.substring(9)); } // 根据配置的视图类型使用对应的方式渲染视图 else { // 使用JSP作为视图 if ("jsp".equals(this.viewType)) { // 将控制器中保持的变量设置到Request中 for (Map.Entry<String, Object> entry : model.entrySet()) { req.setAttribute(entry.getKey(), entry.getValue()); } // 跳转至相应的JSP中 req.getRequestDispatcher(view + ".jsp").forward(req, resp); } // 使用Freemarker作为视图 else if ("freemarker".equals(this.viewType)) { Template template = new Configuration(Configuration.VERSION_2_3_0).getTemplate(view + ".ftl"); template.process(model, resp.getWriter()); } // 使用Velocity作为视图 else if ("velocity".equals(this.viewType)) { } // 其他各种视图... } } 复制代码
上述方式是将所有视图解析的操作都在前端控制器中实现. 缺点显而易见:
设计模式中有一种模式叫做策略模式: 将算法独立封装, 使得算法可以自由切换. 换成视图的方式就是: 将每种类型视图的渲染方式封装成独立的策略类(对外提供渲染视图的方法). 前端控制器根据配置找到对应的视图策略类, 调用渲染视图方法.
// 策略接口 public interface ViewResolver { // 渲染视图方法 void render(HttpServletRequest req, HttpServletResponse resp, String viewName, Map<String, Object> model) throws Exception; } 复制代码
// JSP视图解析策略 public class JSPViewResolver implements ViewResolver { // 使用JSP渲染视图 @Override public void render(HttpServletRequest req, HttpServletResponse resp, String viewName, Map<String, Object> model) throws Exception { // 将控制器中保持的变量设置到Request中 for (Map.Entry<String, Object> entry : model.entrySet()) { req.setAttribute(entry.getKey(), entry.getValue()); } // 跳转至相应的JSP中 req.getRequestDispatcher(viewName + ".jsp").forward(req, resp); } } 复制代码
// Freemarker视图解析策略 public class FreemarkerViewResolver implements ViewResolver { // Freemarker配置 private Configuration config = new Configuration(Configuration.VERSION_2_3_0); // 使用Freemarker渲染视图 @Override public void render(HttpServletRequest req, HttpServletResponse resp, String viewName, Map<String, Object> model) throws Exception { Template template = config.getTemplate(viewName + ".ftl"); template.process(model, resp.getWriter()); } } 复制代码
前端控制器如何能够根据配置的视图类型找到对应的策略类并且实例化呢?
在 web.xml
中配置视图类型时使用对应的视图策略类的名称, 前端控制器通过JAVA发射就可以实例化策略类并调用其渲染视图的方法.
在 web.xml
中配置视图策略类
<servlet> <servlet-name>mvc</servlet-name> <servlet-class>com.atd681.xc.ssm.framework.DispatcherServlet</servlet-class> <!-- 视图类型 --> <init-param> <param-name>viewClass</param-name> <param-value>com.atd681.xc.ssm.framework.view.FreemarkerViewResolver</param-value> </init-param> </servlet> 复制代码
在前端控制器中定义视图类型, 并在初始化时加载.
// 视图策略类路径(jsp,freemarker,velocity...) private String viewClass = "com.atd681.xc.ssm.framework.view.JSPViewResolver"; /** * 初始化Servlet. 容器初始化Servlet时调用, 加载配置文件初始化MVC相关组件(控制器,视图解析器等) */ @Override public void init() throws ServletException { // 获取用户自定义的视图策略类路径 String viewClassConfig = getInitParameter("viewClass"); if (viewClassConfig != null) { this.viewClass = viewClassConfig; } } 复制代码
在解析视图时, 根据配置的视图类型使用对应的方式解析视图
private void doService(HttpServletRequest req, HttpServletResponse resp) throws Exception { // 返回的视图 String view = (String) method.invoke(classInstance, methodParams); // 根据返回值判断重定向或渲染视图 if (view.startsWith("redirect:")) { resp.sendRedirect(view.substring(9)); } // 根据配置的视图类型使用对应的方式渲染视图 else { // 实例化对应的策略类 ViewResolver viewResolver = (ViewResolver) Class.forName(this.viewClass).newInstance(); // 使用视图策略类封装的渲染方法 viewResolver.render(req, resp, view, model); } } 复制代码
后续章节讲到Spring IOC容器时, 实例化视图策略可以放到IOC容器.
视图解析使用策略模式后, 大大简化了代码的复杂度. 当需要增加视图或修改视图时只需要增加或修改相应的视图策略类即可. 对前端控制器的逻辑没有任何影响.
策略模式是一种很常见的设计模式, 结合Spring IOC使用非常的简单便捷. 使用策略模式可以大大降低代码的复杂度, Spring IOC下的策略模式天生就是为了解决过多的 if/else
SpringMVC的视图解析为了支持多视图解析, 增加了View的概念, View负责解析视图. ViewResolver负责获取对应的视图. 但整体思路 有兴趣的读者可以看下Spring的源码.