(2)加载用户定制页面的第二种方案
其实在前文(一)已经提到了,把页面、模块、布局都抽象成组件类,就如同Jsp标签一样
这种标签类负责输出每个模块的内容,当然包含了业务数据在里面,具体参考一下几个类
大家会发现:在每个组件类中都强制实现toString方法,其实就是使用FreeMarker的功能,每个组件类都有属性对应ftl模板,并且又有数据属性
所以使用FreeMarker的process方法,传入ftl模板和数据既可以把布局、模板都输出来
这才是为什么设计布局、模块表结构时,为什么要记录其对应的ftl模板路径
当然:要注意一点,因为所有ftl模板是已经定制好的,放在某个固定的目录,可以在系统启动之后先一次加载完所有模板
那么每个组件类只要传入(模板所在路径+数据)即可得到页面
(相信使用过FreeMarker来模拟Hibernate或者MyBatis来做动态SQL的同学都明白我的意思)
所有组件的父类:BaseTag
/** * */ package com.yli.cshop.bean; /** * * 把页面/布局/模块都抽象成一个个组件,就好比Jsp标签一样<br> * BaseTag 则是所有组件的父类,强制子类重写toString方法 * * @author yli * */ public abstract class BaseTag { /** * 重写toString()方法 */ public abstract String toString(); }
负责输出页面的组件类:Page
package com.yli.cshop.bean; import java.util.List; /** * 页面可以认为是所有组件的父容器<br> * 它可以包含有固定部分和动态布局的部分<br> * * @author yli * */ public class Page extends BaseTag { /** * 页面ID-系统生成的ID */ private long pageId; /** * 页面名称-用户可自由添加页面,定义页面名称 */ private String pageName; /** * 页面序号-用户可以自由拖动页面排序,记录该序号 */ private int sortNo; /** * 当前页面包含的布局列表<br> * 布局又包含了模块,所以组成了完整的页面 */ private List<Layout> layoutList; public long getPageId() { return pageId; } public void setPageId(long pageId) { this.pageId = pageId; } public String getPageName() { return pageName; } public void setPageName(String pageName) { this.pageName = pageName; } public List<Layout> getLayoutList() { return layoutList; } public void setLayoutList(List<Layout> layoutList) { this.layoutList = layoutList; } public int getSortNo() { return sortNo; } public void setSortNo(int sortNo) { this.sortNo = sortNo; } public String generateHtml() { return null; } @Override public String toString() { if (null != layoutList) { StringBuffer buffer = new StringBuffer(); for (Layout layout : layoutList) { buffer.append(layout.toString()); } return buffer.toString(); } return null; } }
负责输出布局的组件类:Layout
package com.yli.cshop.bean; import java.util.List; import com.yli.cshop.util.FreeMakerParser; /** * 布局可以认为是模块的父容器<br> * 用户可将模块自由添加进来,并对模块排序<br> * 布局其实也可以看做模块,只是它不负责具体的业务内容而已<br> * * @author yli * */ public class Layout extends BaseTag { /** * 布局ID-系统生成的ID */ private long layoutId; /** * 所属页面ID-建立页面与布局的关系 */ private long pageId; /** * 布局开始html标签-对应一个FreeMarker模板地址 */ private String beginTemplate; /** * 布局结束html标签-对应一个FreeMarker模板地址 * */ private String endTemplate; /** * 布局序号-用户可在页面添加多个布局,并自由排序,记录该序号 */ private int sortNo; /** * 当前布局包含的模块列表 */ private List<BaseModule> moduleList; public long getLayoutId() { return layoutId; } public void setLayoutId(long layoutId) { this.layoutId = layoutId; } public long getPageId() { return pageId; } public void setPageId(long pageId) { this.pageId = pageId; } public String getBeginTemplate() { return beginTemplate; } public void setBeginTemplate(String beginTemplate) { this.beginTemplate = beginTemplate; } public String getEndTemplate() { return endTemplate; } public void setEndTemplate(String endTemplate) { this.endTemplate = endTemplate; } public int getSortNo() { return sortNo; } public void setSortNo(int sortNo) { this.sortNo = sortNo; } public List<BaseModule> getModuleList() { return moduleList; } public void setModuleList(List<BaseModule> moduleList) { this.moduleList = moduleList; } @Override public String toString() { StringBuffer buffer = new StringBuffer(); String beginHtml = FreeMakerParser.process(beginTemplate, null); String endHtml = FreeMakerParser.process(endTemplate, null); buffer.append(beginHtml); for (BaseModule module : moduleList) { buffer.append(module.toString()); } buffer.append(endHtml); return buffer.toString(); } }
特别注意:负责输出模块的组件类:BaseModule
package com.yli.cshop.bean; import com.yli.cshop.util.FreeMakerParser; /** * 虽然模块和布局一样,负责输出一段html标签<br> * 但是模块是负责各自业务模块的,比如有[公告信息模块][轮播广告模块]<br> * 意味着每个模块还会关联各自的业务内容,即关联到业务表<br> * 但是每个业务模块要[保存/更新/删除]的业务内容:所对应的的业务类、方法肯定不一样<br> * 所以把模块抽象出来的同时,把每个模块对应的业务类和业务内容定义成两个属性<br> * * @author yli * */ public class BaseModule extends BaseTag { /** * 模板ID-系统生成的ID */ private long moduleId; /** * 布局的ID-建立布局与模块的关系 */ private long layoutId; /** * 模块名称-用户可以自定义模块名称 */ private String moduleName; /** * 模块序号-用户可自由拖动模块,记录该模块在当前布局的序号 */ private int sortNo; /** * 模块对应的FreeMarker模板路径-因为每个模块"长的样子肯定不一样" */ private String moduleTemplate; /** * 模块包含的业务数据-不同的业务模块对应的数据和数据类型不一样 */ private Object moduleContent; /** * 模块对应的业务处理类-此处记录业务Bean名称即可 */ private String moduleServiceBean; public long getModuleId() { return moduleId; } public void setModuleId(long moduleId) { this.moduleId = moduleId; } public long getLayoutId() { return layoutId; } public void setLayoutId(long layoutId) { this.layoutId = layoutId; } public String getModuleName() { return moduleName; } public void setModuleName(String moduleName) { this.moduleName = moduleName; } public int getSortNo() { return sortNo; } public void setSortNo(int sortNo) { this.sortNo = sortNo; } public String getModuleTemplate() { return moduleTemplate; } public void setModuleTemplate(String moduleTemplate) { this.moduleTemplate = moduleTemplate; } public Object getModuleContent() { return moduleContent; } public void setModuleContent(Object moduleContent) { this.moduleContent = moduleContent; } public String getModuleServiceBean() { return moduleServiceBean; } public void setModuleServiceBean(String moduleServiceBean) { this.moduleServiceBean = moduleServiceBean; } @Override public String toString() { StringBuffer buffer = new StringBuffer(); String moduleInfo = FreeMakerParser.process(moduleTemplate, moduleContent); buffer.append(moduleInfo); return buffer.toString(); } }
所有组件类已经写好了,那么就准备布局、模块对应的FreeMarker模板文件即可,如下所示
我为页面、布局和每种模块都定制了一个ftl页面,放在classpath目录的conf目录下
为了做演示,在MySql数据库针对页面、布局和模块分别建表,并插入演示的数据,如下所示
现在数据都准备好了,就可以输出页面了,此处举例说明如何实现
package com.yli.cshop.service.impl; import java.util.HashMap; import java.util.List; import java.util.Map; import com.yli.cshop.bean.BaseModule; import com.yli.cshop.bean.Layout; import com.yli.cshop.bean.Page; import com.yli.cshop.service.BaseModuleService; import com.yli.cshop.service.PageService; import com.yli.sample.base.ServiceImplBase; public class PageServiceImpl extends ServiceImplBase implements PageService{ public Map<String, BaseModuleService> moduleServiceCache; public void setModuleServiceCache( Map<String, BaseModuleService> moduleServiceCache) { this.moduleServiceCache = moduleServiceCache; } public String loadPage(){ Map<String, Object> param = new HashMap<String, Object>(); param.put("pageId", 100001); // 根据页面ID查询Page对象 Page page = sopDalClient.queryForObject("com.yli.cshop.layout.query_page", param, Page.class); // 根据页面ID查询:该页面包含的布局列表,根据布局序号升序排序 List<Layout> layoutList = sopDalClient.queryForList("com.yli.cshop.layout.query_layout_list", param, Layout.class); if(null != layoutList && !layoutList.isEmpty()) { // 设置布局列表到页面对象中 page.setLayoutList(layoutList); // 根据布局ID查询:每个布局包含的模块列表,根据模块所在布局序号升序排序 for(Layout layout : layoutList) { param.put("layoutId", layout.getLayoutId()); List<BaseModule> moduleList = sopDalClient.queryForList("com.yli.cshop.layout.query_module_list", param, BaseModule.class); if(null != moduleList && !moduleList.isEmpty()) { // 设置模块列表到页面对象中 layout.setModuleList(moduleList); for(BaseModule module : moduleList) { // 查询每个模块应该包含的业务内容,此处处理比较复杂 BaseModuleService baseModuleService = moduleServiceCache.get(module.getModuleServiceBean()); Object moduleContent = baseModuleService.getModuleContent(module.getModuleId()); // 将模块的业务内容设置到模块实例中 module.setModuleContent(moduleContent); } } } } // toString() 其实就是使用FreeMarker的process方法达到[数据]+[模板]=[输出]的目的 // 这个pageContent就是整个页面的内容了,只要显示在前端即可 // 不管你用Jstl来读取,还是ajax来读取,都可以 String pageContent = page.toString(); System.out.println(pageContent); // 如果觉得组件类依赖了FreeMarker,被侵入,则也可以抽象出输出页面的工厂类 // page 就只作为保存[数据]和[模板]的功能来设计 // String pageContent = PageFactory.generatePage(page); return pageContent; } }
最后就是效果图了,很简答的一个