展示层
在Macula开发平台下,建议使用Spring MVC + Freemarker的方式来实现展现层。
同时对于Ajax部分,在javascript框架中建议使用jquery框架。
Controller编写
使用基类Controller
在展示层编写的Controller实现,需要直接或间接扩展至BaseController
@RequestMapping("admin/macula-base")
public class AdminMaculaBaseController extends BaseController {
//something
}
在BaseController中处理了大量的异常处理方式以及数据返回要求的设定。
表单校验@Valid
在Controller中的参数中,使用@FormBean注解来绑定页面数据到Bean,如果转换失败,则失败结果会出现在BindingResult中
在Controller中的参数中,使用@Valid注解来检查页面数据到Domain数据是否符合校验规则,校验规则的定义是在Bean中完成的,采用JSR-303的Bean Validator标准定义。校验失败的结果同样保存在BindingResult中
失败结果可以通过BaseController中的getMergedBindingResults方法得到,具体使用请参考BaseController类的使用说明。
表单绑定@FormBean
在Spring MVC默认的基础上,Macula开发平台在参数绑定上做了适当扩展,以适应与Struts(Webwork)等相同的对参数处理的一致性,具体来说,有如下的变化:
Bean参数绑定
默认情况下,String MVC对参数的绑定方式,采用直接属性名与给定POJO属性名相同的方式实现绑定,为了更好的区分具体的参数信息,Macula平台扩展了这类绑定,允许
pojo名+ . + 属性名
的方式绑定。
例 1. 两种绑定的区别
比如在Controller中,会传入用户信息保存,其Controller原型为:
public User save(User user) { // something return user; }
此时客户端提交的参数信息为:
?userName=Wilson&password=123456
此时Spring自动将userName和password绑定生成User对象。但这种方式在返回多个对象时不太适用,所以Macula平台通过扩展,可通过修改Controller中的原型为:
public User save(@Valid @FormBean("user") User user){ if (hasErrors()) { throw new FormBindException(getMergedBindingResults()); } // something return user; }
通过Macula平台扩展后的提交的数据格式,将可以通过下面提交方式绑定:
?user.userName=Wilson&user.password=123456
为实现这个扩展,主要在于applicationContext-mvc.xml文件中的BeanArgumentResolver定义:
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="customArgumentResolvers"> <list> <bean class="org.macula.core.mvc.FormBeanArgumentResolver"> <property name="webBindingInitializer" ref="webBindingInitializer" /> </bean> </list> </property> </bean>
通过对自定义参数的解析,可以实现上述的变化。
重要
需要注意的是,使用Macula平台的绑定方式的前提是:必须使用@FormBean前缀,并且不能使用诸如@ModelAttribute、@RequestBody等Spring的绑定注解。
FormBean的表单防重复提交
在Form提交时,为了防止用户对表单的重复提交,除了使用客户端脚本控制按钮的状态外,平台提供了防重复提交的解决方案。
一般情况下,防重复提交有2个应用场景:1)防止用户无意识的重复提交;2)防止用户恶意的重复提交。
对于无意识的提交,只需要在页面隐藏一个唯一性的Token,在表单提交时返回,在服务端校验并销毁即可完成对Token的验证而阻止该提交。
对于用户可能存在的恶意提交,可通过在表单提交时,插入验证码的方式来进行,用户必须正确输入了验证码,并在服务端校验成功后,才能进行业务逻辑的处理。
在平台实现的方式上,采用在FormBean注解中加入属性:
valid:(boolean)是否需要检测重复提交;
token:(String)在表单页面中提交的参数名称,默认值为ftoken,除非与业务中的字段冲突,否则不需要设置为其他值;
captcha:(boolean)是否检测验证码,来防止恶意提交
整个校验过程由FormBeanArgumentResolver完成。
相应的,在界面层面,需要配合在表单中加入防重复提交信息,在macula.ftl中提供了freemarker宏的默认实现。在该默认实现情况下,可通过在表单位置加入<@macula.formToken />即可,对于需要加入校验码的情况下,使用<@macula.formToken captcha=true />
具体的实现可参考macula.ftl文件。
重要
主要注意在同一个RequestMapping的方法中,如果有多个通过@FormBean注释的参数,在第一个使用FormBean注释的参数中加入该特性即可,其他不要加。
特别地,加入了自动控制防重复提交后,生成的客户端token只能进行一次校验即失效,所以在提交后,如果表单需要再次提交,需要更新隐藏的token的值。在默认情况下,调用$(form).trigger(‘changeCaptcha’)即可更新,如果需要定制,可参考macula.ftl中的实现,做自定义的宏来处理。
HTML表单input name与后端参数转换规则
本节继续上级讲解复杂的@FormBean对象是如何与HTML表单数据绑定的,假如:
@OpenApi
@RequestMapping(...)
public void saveUser(@FormBean User user) {
}
User类如下:
public class User {
private String userName;
private String password;
private Org org;
private List<Org> orgs;
private Map<String, String> params;
private Map<String, Org> girls;
private Date date;
}
public class Org {
private String code;
}
那么HTML表单中input框的name应该如下命名:
<input name="user.userName" >
<input name="user.password" >
<input name="user.org.code" >
<input name="user.orgs[0].code" >
<input name="user.orgs[1].code" >
<input name="user.params['key1']" >
<input name="user.params['key2']" >
<input name="user.girls['key1'].code" >
<input name="user.girls['key2'].code" >
<input name="user.date" >
原子类型:例如long,int,String等,需要形成“参数名=值”的键值对传递;
POJO对象:如User,需要形成“参数名.属性名=值”的键值对传递;
原子类型数组:如String[]、List,需要形成“参数名=值0”、“参数名=值1”等的键值对传递;
POJO对象数组:如User[]、List,需要形成“参数名[index].属性名=值”等的键值对,index为数组下标;
Map:需要形成“参数名[key]=值”的键值对传递;
Map:需要形成“参数名[key].属性名=值”的键值对传递;
对于POJO对象中如果含有POJO数组、Map、POJO则规则同上,而原子类型数组需要写成“属性名[index]=值”。
Pageable参数绑定
在使用了Spring-Data框架够,对于多数分页式查询,可通过直接传入Pageable参数和额外的参数条件,即可返回包括总记录数、当前页面记录等信息的Page对象返回,对于Controller层,方便的获得页面传递的Pageable参数并构造成相应的对象值,也是一种代码简洁和易用性上的提升。
对于Pageable参数的绑定,比如Controller中编写:
@RequestMapping(value = "/test/user/list", method = RequestMethod.GET) public Page<User> list(Pageable pageable) { Page<User> page = userRespository.findAll(pageable); // other coding... return page; }
为了实现这个扩展,主要在applicationContext-mvc.xml文件中的PageableArgumentResolver定义:
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters" ref="messageConverters" /> <property name="webBindingInitializer" ref="webBindingInitializer" /> <property name="customArgumentResolvers"> <list> <bean class="org.macula.core.mvc.PageableArgumentResolver" /> </list> </property> </bean>
Pageable参数绑定时,将直接从Request参数中获取,如果在一个方法中,需要构建多个Pageable对象,可通过@Qualifier来指定别名,这样在Request中获取属性 别名+ “_” + 属性名,来构建Pageable对象。
request参数包括:page(页码)、rows(每页行数)、sort(按什么排序,格式是field1,field2…,asc或者desc,可以多个sort参数,最后一个表示排序类型)
重要
这里Pageable与Bean构建的区别在于,默认情况下Pageable直接从Request中获取数据,而在通过@Qualifier指定别名时,Bean的属性获取规则是 别名+ “.” + 属性名,而Pageable的规则是 别名+ “_” +属性名。
ID到Domain类型转换
很多情况下,在编辑时或者在查看详细信息时,总是通过传入一个主键值(通常是Long型),来获取具体的记录信息,在Macula平台中,为了简化这种操作,对于已定义的Domain类,可以通过已定义的ConversionService直接转换。
对应的applicationContext-mvc.xml中配置如下:
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<list>
<bean class="org.macula.core.mvc.RepositoryConverter" />
</list>
</property>
</bean>
配置该转化后,需要转化的类型必须实现Persistable接口,并且定义了相对应的JpaRepository,否则也不能正常转换。
重要
除了加入该ConversionService外,还需要注意:
普通的VO对象不要实现Persistable接口,即不能使用该转换
待转化类必须实现Persistable接口
该转换Domain对象,在Spring上下文中,已经定义了相应的JpaRepository Bean,用来通过主键载入该对象值
例 2. 通过传入主键,直接转化为相应的对象
@RequestMapping(value = "/test/user/{userId}/edit", method = RequestMethod.GET) public User edit(@PathVariable("userId") User user) { return user; }
如上面的Controller中的定义可见,传入的userId是一个字符串(或者可以认为是Long型),但在edit方法中,可直接定义为User user,即由macula平台实现了对主键到相应Domain实例的转换。
当然,这里的User对象实现了Persistable接口,并已有相应的UserRepository extends
JpaRepository<User>的实现。
ExcelView
为了更好的支持Excel的导出功能,系统提供了ExcelView类结合ExcelUtils的Excel模板方式导出Excel。只要按照ExcelUtils的语法制作Excel模板,然后放在FreeMarker模板文件放置的目录中,在Controller中如下使用:
public ModelAndView excel2() {
Map<String, Object> model = new HashMap<String, Object>();
model.put("name", "变量");
return new ModelAndView(new ExcelView(getRelativePath("/so_master/excel")), model);
}
上述代码将会去views/admin[front]/xxxx/so_master/目录下寻找excel.xls的Excel模板文件,然后通过ExcelUtils解析该模板文件生成需要的Excel文件。
Freemarker模板编写
页面布局
Macula 使用 FreeMarker 宏来规范ftl页面模板的开发:
- 后端默认布局界面编写
<@layout.mower_admin title="Dashboard">
具体业务内容
</@layout.mower_admin>
- 前端默认界面布局编写:
<@layout.mower_front title="Dashboard">
<@ui.main_breadcrumb rootType='front' menuCode="FRONT_TEST_MENU1" />
<@ui.main_wrapper>
<@ui.main_content>
Dashborad
</@ui.main_content>
</@ui.main_wrapper>
</@layout.mower_front>
重要
布局可以根据是否AJAX请求自动判断是否输出javascript和css的脚本
页面布局宏的定义
我们先看看freemarker整个宏文件的定义逻辑:
在views根目录下,有个layout.ftl是整个布局宏的入口:
<#macro includeScripts scripts>
<#if scripts?exists && scripts != ''>
<#list scripts?split(",") as jsItem>
<script id="${jsItem?trim?replace('.', '_')?replace('/', '_')}" type="text/javascript">
<#include "/${jsItem?trim?replace('.js', minVersion+'.js')}" parse=false />
</script>
</#list>
</#if>
</#macro>
<#include "/admin/layout.ftl" />
<#include "/admin/layout_mower.ftl" />
<#include "/front/layout_mower.ftl" />
<#include "/mobile/layout_mower.ftl" />
admin目录下,layout.ftl是macula2.0版本的界面布局,layout_mower.ftl是macula 3.0的布局宏定义,下面讲讲这个文件:
<#global resources_admin="${resourceHost!''}/resources/mower/${appVersion!''}/admin" />
<#global views_admin="${resourceHost!''}/views/mower/${appVersion!''}/admin" />
<#macro mower_admin_head title = '' require = ''>
meta、css等脚本定义
</#macro>
<#macro mower_admin_header_logo>
...
</#macro>
<#macro mower_admin_header_menu>
...
</#macro>
<#macro mower_admin_header_login>
...
</#macro>
<#macro mower_admin_footer>
...
</#macro>
<#macro mower_admin_scripts require = ''>
js引入定义
</#macro>
<!--给用户自定义宏的机会 -->
<#include "/admin/app/layout_mower.ftl" />
<#macro mower_admin title scripts = '' version = '' require = ''>
<#global ui_name = 'mower' />
<#global ui_path = 'admin' />
<#if Request['isAjaxRequest']?exists && Request['isAjaxRequest'] == true>
<#if title?exists><title>${title?if_exists}</title></#if>
<#if version?exists><meta content="${(version?replace("(\\$)|(\\s\\$)|(Revision:\\s)", "", "r"))?if_exists}-${appVersion!""}"></#if>
<#nested />
<@includeScripts scripts />
<#else>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<@mower_admin_head title="${title?if_exists}" require = "${require!''}" />
<script type="text/javascript">
...
</script>
</head>
<body data-base="${base}">
<noscript>
<div class="noscript error">
您好,要正常运行应用程序,浏览器必须支持Javascript!
</div>
</noscript>
<!-- Loading Container -->
<div class="loading-container">
<div class="loader"></div>
</div>
<!-- /Loading Container -->
<!-- BEGIN HEADER -->
<div id="header" class="mu-header-container">
<!-- BEGIN MAIN MENU -->
<div class="navbar" role="navigation">
<div class="navbar-inner">
<div class="navbar-container">
<@mower_admin_header_logo />
<!-- Sidebar Collapse -->
<div class="sidebar-collapse" id="sidebar-collapse">
<i class="collapse-icon fa fa-bars"></i>
</div>
<!-- /Sidebar Collapse -->
<@mower_admin_header_menu />
<@mower_admin_header_login />
</div>
</div>
</div>
</div>
<!-- END HEADER -->
<!-- BEGIN MAIN -->
<div class="mu-main-container container-fluid">
<!-- BEGIN SIDEBAR -->
<div class="mu-sidebar-wrapper">
<div class="mu-sidebar" id="sidebar">
<!-- Page Sidebar Header-->
<div class="sidebar-header-wrapper">
<input type="text" class="searchinput">
<i class="searchicon fa fa-search"></i>
<div class="searchhelper">搜索菜单</div>
</div>
<!-- /Page Sidebar Header -->
<!-- Sidebar Menu -->
<ul class="nav sidebar-menu" style="">
</ul>
<!-- /Sidebar Menu -->
</div>
</div>
<!-- END SIDEBAR -->
<!-- BEGIN CONTENT -->
<div class="mu-content-wrapper">
<div class="mu-content">
<!-- BEGIN BREADCRUMB -->
<div class="mu-content-header">
<div class="mu-breadcrumb-wrapper">
<ul class="breadcrumb" data-target="#mainContent">
<li data-target="breadcrumb0" class="active"></li>
</ul>
</div>
<div class="header-buttons">
<a class="sidebar-toggler" href="#">
<i class="fa fa-arrows-h"></i>
</a>
<a class="refresh" id="refresh-toggler" href="">
<i class="glyphicon glyphicon-refresh"></i>
</a>
<a class="favorite" id="favorite-toggler" href="#">
<i class="fa fa-star fa-lg"></i>
</a>
</div>
</div>
<!-- END BREADCRUMB-->
<div class="mu-content-body">
<div class="row">
<div class="col-md-12">
<div id="mainContent">
<div data-panel="breadcrumb0">
<#nested />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END MAIN -->
<!-- BEGIN FOOTER -->
<div id="footer" class="mu-footer">
<@mower_admin_footer />
</div>
<!-- END FOOTER -->
<@mower_admin_scripts require = "${require!''}" />
<!-- BEGIN PAGE LEVEL SCRIPTS -->
<script type="text/javascript" src="${resources_admin}/app/js/config${minVersion!""}.js"></script>
<script type="text/javascript" src="${resources_admin}/app/js/app${minVersion!""}.js"></script>
<@includeScripts scripts="admin/app/layout_mower.js" />
<@includeScripts scripts="ui/app/ui.js" />
<@includeScripts scripts />
<!-- END PAGE LEVEL SCRIPTS -->
</body>
</html>
</#if>
</#macro>
提示
<@layout.mower_admin title scripts = ‘页面脚本’ version = ’’ require = ‘angularjs/knockoutjs/vue’>
</@layout.mower_admin>
scripts是您的页面对应的脚本,路径以views下面的目录为准
title 页面标题
version 页面版本
require 要引入的MVC JS文件,支持angularjs、knockoutjs、vue
这个宏是业务页面的主要的宏,一般情况下,不要脱离这个宏做页面,这样未来修改时可以整体替换。
后端页面布局定制
下面我们以后台管理页面为例进行讲解。后台管理页面布局如下图所示:
由上图可见,Macula 页面由 header,main container 和 footer 三部分组成。其中,header 由 logo,menu 和 login 组成;main container 主要包括 sidebar 和 content 两大部分;footer 构成比较简单。
为了方便大家理解,我们以一个实际的页面为例子说明各个部分。
开发者可以通过修改自己项目中的如下这个文件来自定义自己的 header logo,header menu,header login 和 footer。
app目录中的layout_mower.ftl文件可以覆盖,同时会重新定义admin/layout_mower.ftl中的相关宏。
我们来看一下admin/app目录下的这个文件里面的内容:
<#--
使用者可以通过覆盖这个文件实现对一些宏的重新定义包括:
-- html head中的内容,包括meta css等
<#macro mower_admin_head title = ''>
-- LOGO
<#macro mower_admin_header_logo>
-- 菜单栏
<#macro mower_admin_header_menu>
-- 登录后提示信息
<#macro mower_admin_header_login>
-- 页脚
<#macro mower_admin_footer>
包含的Javascript(angularjs,knockoutjs,datagrid)
<#macro mower_admin_scripts require = ''>
-->
<#--
局部替换:如果以下变量定义在具体业务模板中,则会覆盖你的layout模板中的定义
<#global mower_admin_scripts_addition>
加入你自己的javascript库文件
</#global>
<#global mower_admin_head_addition>
加入你自己的css文件
</#global>
-->
由上面的代码可见,我们可以通过修改宏 mower_admin_header_logo 来自定义自己的 header logo;同样道理我们可以通过修改宏 mower_admin_header_menu,mower_admin_header_login,以及 mower_admin_footer 来分别定义自己的 header menu,header login 和 footer。
前端页面布局定制
与后端定制类似,front/app/layout_mower.ftl文件提供了前端布局定制功能:
<#--
使用者可以通过覆盖这个文件实现对一些宏的重新定义包括:
--html head中的内容,包括css meta等
<#macro mower_front_head title = ''>
--页面的最上端
<#macro mower_front_header_login>
--广告
<#macro mower_front_header_ad>
--LOGO
<#macro mower_front_header_logo>
--菜单栏
<#macro mower_front_header_menu>
--页脚
<#macro mower_front_footer>
--包含的Javascript(angularjs,knockoutjs,datagrid)
<#macro mower_front_scripts require = ''>
-->
<#--
局部替换:如果以下变量定义在具体业务模板中,则会覆盖你的layout模板中的定义
-- 定义最顶的登录状态条最左边显示的文字内容
<#global mower_front_header_login_custom_left>
文字
</#global>
-- 定义最顶的登录状态条最右边显示的文字内容
<#global mower_front_header_login_custom_right>
<li>
<a target="_blank" href="#"><i class="fa fa-sitemap"></i>网站导航</a>
</li>
</#global>
定制广告头
<#global mower_front_header_ad_custom>
</#global>
定制LOGO区域
<#global mower_front_header_logo_custom>
</#global>
定制整个菜单区域
<#global mower_front_header_menu_custom>
xxxxx
</#global>
定制默认菜单的右边区域
<#global mower_front_header_menu_custom_right>
<li><a href="xxx">xxx</a></li>
</#global>
定制页脚区域
<#global mower_front_footer_custom>
ddddadf
</#global>
<#global mower_front_scripts_addition>
加入你自己的javascript库文件
</#global>
<#global mower_front_head_addition>
加入你自己的css文件
</#global>
-->
UI宏
u目录的ui.ftl提供了框架默认的UI宏,包括:
<#-- UI的自定义宏 -->
<#---------------------------------------ADMIN--------------------------------------->
<#macro panel id='panel-list' class='panel panel-widget unboxed'>
</#macro>
<#macro panel_head>
</#macro>
<#macro panel_body>
</#macro>
<#macro panel_footer>
</#macro>
<#macro panel_toolbar_query>
</#macro>
<#macro panel_toolbar_advanced_query>
</#macro>
<#------------------------------FRONT------------------------------------->
<#macro main_breadcrumb rootType = 'admin' menuCode = ''>
<!-- BEGIN BREADCRUMB -->
<!-- END BREADCRUMB-->
</#macro>
<#macro main_sidebar>
</#macro>
<#macro main_content>
</#macro>
<#macro main_wrapper>
</#macro>
<#----------------------------------------COMMON--------------------------------->
<#include "/ui/app/ui.ftl" />
典型的后端页面是:
<@layout.mower_admin title='' scripts=''>
<@ui.panel>
<@ui.panel_head>
工具条
</@ui.panel_head>
<@ui.panel_body>
内容
</@ui.panel_body>
</@ui.panel>
</@layout.mower_admin>
典型的前端页是:
<@layout.mower_front title="Dashboard">
<@ui.main_breadcrumb rootType='front' menuCode="FRONT_TEST_MENU1" />
<@ui.main_wrapper>
<@ui.main_content>
Dashborad
</@ui.main_content>
</@ui.main_wrapper>
</@layout.mower_front>
你可以覆盖ui/app/ui.ftl和ui.js来定义你的UI宏。
高级查询自动关联
这个以一个例子来说明。如下图所示,有一个根据名称和编码的快捷查询,在其右侧还有一个高级查询,点击高级查询按钮,dropdown里面的查询条件更丰富一些。用户在快捷查询中的名称、编码填了内容后,发现当前条件还没能筛选出满意的内容,当他点击高级查询时,在高级查询dropdown里的相应字段(菜单名称、编码)的内容会用快捷查询的相应内容自动填充,并把它们的值关联起来。
这个主要是通过框架中定义的panel_toolbar_query和panel_toolbar_advanced_query这两个宏实现的。两个宏中的input元素通过id进行value的关联。程序会遍历panel_toolbar_query中的input元素的id,然后在panel_toolbar_advanced_query找到前缀为advanced_query_相应的id进行关联。如下例中的id分别为“name”和“advanced_query_name”的两个元素。
<@ui.panel_toolbar_query>
<form class="form-inline" id="form-search">
<label style="color: #333;">名称</label>
<input type="text" class="input-sm form-control" size="5" id="name" name="name">
<label style="color: #333;">编码</label>
<input type="text" class="input-sm form-control" size="5" id="code" name="code">
<a href="#" id="search-submit-action-${code}" class="btn btn-default btn-sm">
<i class="fa fa-search"></i>
</a>
</form>
</@ui.panel_toolbar_query>
<@ui.panel_toolbar_advanced_query>
<form class="form-horizontal" id="form-advanced-search">
<div class="form-body" style="width: 420px; height: 280px; padding: 10px 30px 0 0; text-align: right;">
<div class="form-group row">
<label class="col-md-4" style="padding-top: 5px">菜单名称</label>
<div class="col-md-8">
<input type="text" class="form-control input-sm" maxlength="50" id="advanced_query_name" name="name"/>
</div>
</div>
…
…
</@ui.panel_toolbar_advanced_query>
另外,panel_toolbar_advanced_query有个参数“customDivId”,可以让我们把高级查询的内容放到一个自定义的div里而不是默认的dropdown中,当点击“高级查询”时div会自动toggle,相应的字段也会关联。
<div class = "row">
<@ui.panel_toolbar_query>
<form class="form-inline" id="form-search">
<label style="color: #333;">名称</label>
<input type="text" class="input-sm form-control" size="5" id="name" name="name">
<label style="color: #333;">编码</label>
<input type="text" class="input-sm form-control" size="5" id="code" name="code">
<a href="#" id="search-submit-action-${code}" class="btn btn-default btn-sm">
<i class="fa fa-search"></i>
</a>
</form>
</@ui.panel_toolbar_query>
<@ui.panel_toolbar_advanced_query customDivId="myDiv">
</@ui.panel_toolbar_advanced_query>
</div>
</div>
<div id="myDiv">
…
…
</div>
前端开发框架
Macula 使用 Mower 作为前端开发框架。有关 Mower 的详细介绍请访问 Mower 官方网站。
Macula宏
为了方便开发,框架内置了一些常用的macula宏给freemarker使用
权限判断
在freemarker中,如果需要通过权限控制是否显示内容,如下:
<@macula.preAuthorized url method>
有权限的显示
</@macula.preAuthorized>
<@macula.notAuthorized url method>
没有权限的显示
</@macula.notAuthorized>
下拉框
我们先来看看Macula中下拉框的样子,如下图绿色方框中所示:
要实现下拉框首先需要有下拉框选项。下拉框的选项是静态的情形很简单,在这里我们讨论的是下拉框中的选项是从数据库中获取的。我们知道下拉框的选项由 name 和 value两项组成。在这里,我们需要用到 Macula 的数据参数功能。我们可以在数据参数中定义这些选项。数据参数中定义的选项有三种形式。
典型的形式如下:
name1:value1|name2:value2|...
例如:
NONE:不缓存|SESSION:整个用户Session作用域|INSTANCE:实例级作用域|APPLICATION:全局级别作用域
如果选项的 name 和 value 相同,还可以简化成以下的形式:
name1|name2|...
例如:
String|Integer|Long|Double|Boolean|Timestamp|Date|Word
当然还可以用 SQL 的形式从数据库中获取。例如:
!select app_name as label, app_id as code from ma_base_application
有关数据参数的详细介绍,请参阅“基础插件”中的“数据提供”一节。
下面我们来看下如何在 ftl 中定义下拉框:
<div class="form-group">
<label class="control-label col-md-3">所属应用:</label>
<div class="col-md-9">
<select name="dataParam.appId" data-bind="options: appIdParams.application_list, optionsText: 'label', optionsValue:'code', optionsCaption: '请选择', value: appId " class="chosen-select form-control"/>
</div>
</div>
上面的例子中用到了 knockoutJs 的 data-bind,通过 options 属性将一个名为 appIdParams.application_list 的 js 变量绑定到下拉框的选项中,而 appIdParams.application_list 中的内容正是来自于预先定义好的数据参数 application_list。在同一个 ftl 中我们使用 Macula 框架提供的宏 writeDataParamsJs 获取数据参数 application_list 的内容。如下:
<script type="text/javascript">
var appIdParams={<@macula.writeDataParamsJs 'application_list' />};
</script>
数据参数 application_list 的定义如下:
select app_name as label, app_id as code from ma_base_application
提示:
<@macula.writeDataParamsJs ‘xxx’/>可以通过逗号分隔多个参数,如果同一个界面有多个参数可以通过这种方式一次获取参数。
避免重复提交的Token
<@macula.formToken captcha="true/false" />
上述代码放入表单中,则会自动生成防止重复提交的token,如果要显示验证码,captcha输入true。
FreeMarker内置变量
macula框架通过扩展freemarker提供了一些内置变量和方法给Freemarker模板使用。
登录用户信息
变量userPrincipal:代表用户登陆后的凭据。
登录用户信息在登录时,我们可以通过实现 CustomUserLoginRepository 接口把用户信息放到UserPincipal的atrribute中,可以在Freemarker中通过如下获取,Freemaker中的userPrincipal对应UserPrincipal类:
<#if userPrincipal.getAttributeValue("userInfo")?exists>
<#assign userInfo = userPrincipal.getAttributeValue("userInfo")>
</#if>
<#if userInfo?exists>
<#else>
</#if>
<!--当前用户名-->
${userPrincipal.getName()!""}
<!--当前用户姓名-->
${userPrincipal.getNickname()!""}
菜单变量
macula框架在Freemarker中默认有如下变量:
后端
adminRootMenu :后端根菜单
adminMainMenu:父亲是根的二级菜单
前端
frontRootMenu:前端根菜单
frontMainMenu:父亲是根的二级菜单
移动端
mobileRootMenu:移动端根菜单
mobileMainMenu:父亲是根的二级菜单
获取绝对地址方法
Freemarker内置方法
getDataParams(dataParamCode) 返回FieldOption类型的code/value参数列表;
getMenuBreadcrumb(rootType, menuCode) 返回指定菜单的面包屑结构菜单,参考ui宏
hasAccess(url, method) 返回指定的url和method是否可以访问
getAbsoluteUrl(base, url, default) 返回url的绝对路径,如果url不是以http/https开头,会自动加上base
国际化
国际化 可分为页面国际化和提示信息国际化。
页面国际化可以通过多个Freemarker文件解决,通过不同的国际化后缀来区分不同地区的页面;比如:index.ftl,如果要添加一个英文页面,可以添加index_en_US.ftl,这样当英文国家的用户访问系统时,将最先使用index_en_US.ftl文件。
提示信息国际化使用资源文件处理,在每个模块的资源文件目录下,都有i18n/xxxx/messages_xx_XX.properties等众多资源文件,同时添加到applicationContext-app.xml配置文件中。
地址规划
对于当前大部分的业务系统,存在终端使用和后台管理的情况以及未来对于F5在地址分发方面的合理性布局,在地址规划上,需要按一定的规则进行:
- /admin :如果该功能是一个后台管理功能,则需要在地址前端加入/admin
- /front:如果该功能是一个用户使用功能,则需要在地址前端加入/front/
- /模块名/:针对macula平台开发的需要,每个模块都必须有自己的地址命名空间,对于该部分的命名,需要在模块定义规划时指定(具体的模块命名可能需要进行流程方面的审批)。
- /功能名称/:针对模块下的某一功能,需要给出功能的名称。
- /操作名称/:针对某一功能下具体的操作,需要给出操作的名称,如index,new,edit,save,read,delete,query等动词。
所以最终的地址命名为:
- 管理功能:/admin/模块名/功能名/操作名称/参数/其他
- 用户功能:/front/模块名/功能名/操作名称/参数/其他
请求方式规划
为了保证业务系统不被重复的请求以及不正确的请求干扰,对于请求方式做如下规划:
- 对于获取单条数据或显示新增与编辑页面的方式可以使用GET请求
- 对于删除数据、保存数据或提交多条数据给后台的应该使用POST方式
- 有多个查询条件的查询功能应该使用POST方式
RESTful API
在对REST的支持方面,使用Spring的REST解决方案,macula平台提供@OpenApi注解替换@ResponseBody注解,这里说明在能使用REST的方式下,尽量使用REST方式。
在Macula平台开发中,将不通过地址中的参数来传递参数值,而直接通过地址信息来传递参数值。
如请求的地址:/admin/macula-uim/user/delete/user1 可通过Controller中定义
@RequestMapping(value = "/admin/macula-uim/user/delete/{userName}", method = RequestMethod.DELETE)
@OpenApi
public ExecuteResponse delete(@PathVariable String userName) {
//do something
}
REST数据返回格式
为了未来能够将目前的Controller请求方法开放给其他终端使用,有必要对Controller的返回值做一个统一的规划,如下:
public class Response {
/** 是否成功标识 */
private boolean success;
/** 系统级错误代码 */
private String errorCode;
/** 系统级错误信息 */
private String errorMessage;
/** 业务级错误代码 */
private String exceptionCode;
/** 业务级错误信息 */
private String exceptionMessage;
/** 异常详细信息 */
private String exceptionStack;
/** 服务端重定向信息 */
private String redirection;
/** 校验结果信息 */
private List<FieldError> validateErrors;
public Response() {
this.success = true;
}
public Response(MaculaException exception) {
this.success = false;
this.errorCode = exception.getParentCode();
this.errorMessage = ApplicationContext.getMessage(errorMessage);
this.exceptionCode = exception.getMessage();
this.exceptionMessage = exception.getLocalizedMessage();
this.exceptionStack = exception.getFullStackMessage();
if (exception instanceof FormBindException) {
List<FieldError> fieldErrors = ((FormBindException) exception).getFieldErrors();
for (FieldError fieldError : fieldErrors) {
this.addValidateError(fieldError);
}
}
}
}
public class ExecuteResponse<T> extends Response {
/** 结果信息 */
private final T returnObject;
public ExecuteResponse(T result) {
this.returnObject = result;
}
/**
* @return the result
*/
public T getReturnObject() {
return returnObject;
}
}
public class PageResponse extends Response {
/** 本次请求的记录数 */
private final int size;
/** 当前页码,从零开始 */
private final int number;
/** 总记录数 */
private final long totalElements;
/** 总页数 */
private final int totalPages;
/** 本页的总记录数 */
private final int numberOfElements;
/** 是否首页 */
private final boolean firstPage;
/** 是否最后页 */
private final boolean lastPage;
/** 内容列表 */
private final List<?> content;
public PageResponse(Page<?> page) {
this.size = page.getSize();
this.number = page.getNumber();
this.totalElements = page.getTotalElements();
this.totalPages = page.getTotalPages();
this.numberOfElements = page.getNumberOfElements();
this.firstPage = page.isFirstPage();
this.lastPage = page.isLastPage();
this.content = page.getContent();
}
}
上述代码中,Response类是基类,出现异常时会构造Response类型返回,ExecuteResponse主要用在单记录数据的返回,PageResponse则用于需要返回列表数据的情况。
重要
为了减少对编程的干扰,正常情况下,Controller中的方法可以仍然按照Service接口中的方法的返回值正常返回数据,对于原使用@ResponseBody注解的方法,如果需要,则通过使用@OpenApi注解来自动处理对应的返回值,默认情况下,采用@OpenApi 注解后,非Response、Map、Model等类型的返回值,会被包裹成ExecuteResponse,而Page<?>返回值会被包裹成PageResponse。
@OpenApi注解的启用需要配置RequestMappingHandlerAdapter的customReturnValueHandlers属性:
<property name="customReturnValueHandlers">
<list>
<bean class="org.macula.core.mvc.OpenApiReturnValueHandler">
<constructor-arg ref="messageConverters"/>
</bean>
</list>
</property>