本产品使用了SSM开源框架来架构系统,使用Maven来构建和管理项目。良好的后端架构易于复用、维护和扩展,团队开发人员共用一套后端架构,易于沟通交流、code review。后端使用了了MVC三层结构,分别为Action控制层(V)、BO业务层(C)、Mapper数据访问层(M)。
正文:
1、使用MVC三层结构
Action控制层(V)、BO业务层(C)、Mapper数据访问层(M)。
2、Action层
(1)主要是对业务逻辑的处理(session),其中调用BO层接口。
(2)后端用对象接收http请求的参数数据,统一规范,方便之后的格式检查。
(3)调用BO层时,统一先调用crudObject方法(如createObject),统一规范,防止忘记后面的数据检查。
(4)定义错误码。
系统运行时会遇到各种错误,将这些错误分类,分别定义对应的错误码可以更加清晰描述问题所属分类和方便后面遇到问题时的精准定位。
错误码采用枚举类的方式定义:
public enum EnumErrorCode {
EC_NoError("No error", 0), //
EC_Duplicated("Duplicated", 1), //
EC_NoSuchData("No such data", 2), //
EC_OtherError("Other error", 3), //
EC_Hack("Hack", 4), //
EC_NotImplemented("Not implemented", 5), //
EC_ObjectNotEqual("Object not equal", 6), //
EC_BusinessLogicNotDefined("Business logic not defined", 7), //
EC_WrongFormatForInputField("Wrong format for input field", 8), //
EC_Timeout("Time out", 9), //
EC_NoPermission("No permission", 10), //
EC_OtherError2("Other error 2", 11),//
EC_PartSuccess("Part success", 12),
EC_Redirect("Redirect", 13),
EC_DuplicatedSession("Duplicated session", 14), //你已经在其它地方登录
EC_InvalidSession("Invalid session", 15), // 代表会话无效,一般是因为已经断网
EC_SessionTimeout("Session time out", 16),// 会话过期
EC_WechatServerError("Wechat Server Error", 17);
private String name;
private int index;
private EnumErrorCode(String name, int index) {
this.name = name;
this.index = index;
}
public static String getName(int index) {
for (EnumErrorCode c : EnumErrorCode.values()) {
if (c.getIndex() == index) {
return c.name;
}
}
return null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
(5)接口的一些规范要求。
(5.1)接口必须返回的信息,错误码和错误信息,存放在hashmap中。
Map<String, Object> params = new HashMap<String, Object>();
params.put(BaseAction.JSON_ERROR_KEY,EnumErrorCode.EC_NoError.toString());
params.put(KEY_HTMLTable_Parameter_msg,staffBO.getLastErrorMessage());
(5.2)接口返回字符串的格式。
return JSONObject.fromObject(params, JsonUtil.jsonConfig).toString();
(5.3)遇到错误的地方(如操作数据库失败)需要打印错误日志logger.error()。
if (messageBO.getLastErrorCode() != EnumErrorCode.EC_NoError) {
logger.error("创建登录上班消息失败!" + messageBO.printErrorInfo());
return;
}
(6)共用常量和共用方法的抽取。
共用常量和共用方法放在父类,可以方便子类的调用,避免系统出现大量的重复性代码:
public static final String DEV_DOMAIN = "https://dev.wx.bxit.vip/";
public static final String SIT_DOMAIN = "https://sit.wx.bxit.vip/";
protected Staff getStaffFromSession(HttpSession session) {
Staff staff = (Staff) session.getAttribute(EnumSession.SESSION_Staff.getName());
assert staff != null;
logger.info("当前使用的staff=" + staff);
return staff;
}
(7)模板设计模式思想的使用:
在模板方法模式里,是把不能确定实现的步骤延迟到子类去实现。
在BaseAction类定义getBO()的方法,但是却不实现它,延迟到子类去实现:
protected BaseBO getBO() {
throw new RuntimeException("Not yet implemented!");
}
这里是不确定使用那个BO的,需要子类去指定:
getBO().retrieveNObject((useSYSTEM ? BaseBO.SYSTEM : getStaffFromSession(session).getID()), BaseBO.CASE_CheckUniqueField, bm);
如StaffAction继承了BaseAction,实现了BaseBO:
@Override
protected BaseBO getBO() {
return staffBO;
}
开发人员如果忘记实现getBO()方法,运行测试时可以发现抛出的异常信息:Not yet implemented!,可以起到指引去修改代码的作用。
(8)定义枚举用户域,目标会话是否有权限访问当前Action。
(8.1)枚举类 EnumUserScope。
/* 用户域。用于在Action层进行权限控制 */
public enum EnumUserScope {
STAFF("STAFF", 1), //
BXSTAFF("BXSTAFF", 2), //
VIP("VIP", 4), //
POS("Pos", 8), //
ANYONE("ANYONE", 16); // 任何人都能访问当前Action
private String name;
private int index;
private EnumUserScope(String name, int index) {
this.name = name;
this.index = index;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public static String getName(int index) {
for (EnumEnv e : EnumEnv.values()) {
if (e.getIndex() == index) {
return e.name;
}
}
return null;
}
}
(8.2)判断目标会话的方法canCallCurrentAction。
/**
* @param whoCanCallCurrentAction
* 必须提供的会话的组合
* @return true,当前会话中有目标会话,有权限访问当前action;false,当前会话中没有目标会话,无权限访问当前action
*/
protected boolean canCallCurrentAction(HttpSession session, int whoCanCallCurrentAction) {
do {
if ((whoCanCallCurrentAction & EnumUserScope.ANYONE.getIndex()) == EnumUserScope.ANYONE.getIndex()) {
return true;
}
if ((whoCanCallCurrentAction & EnumUserScope.STAFF.getIndex()) == EnumUserScope.STAFF.getIndex()) {
if (session.getAttribute(EnumSession.SESSION_Staff.getName()) != null) {
return true;
}
}
if ((whoCanCallCurrentAction & EnumUserScope.BXSTAFF.getIndex()) == EnumUserScope.BXSTAFF.getIndex()) {
if (session.getAttribute(EnumSession.SESSION_BxStaff.getName()) != null) {
return true;
}
}
if ((whoCanCallCurrentAction & EnumUserScope.VIP.getIndex()) == EnumUserScope.VIP.getIndex()) {
if (session.getAttribute(EnumSession.SESSION_Vip.getName()) != null) {
return true;
}
}
if ((whoCanCallCurrentAction & EnumUserScope.POS.getIndex()) == EnumUserScope.POS.getIndex()) {
if (session.getAttribute(EnumSession.SESSION_POS.getName()) != null) {
return true;
}
}
} while (false);
return false;
}
(9)在Action层调用该方法canCallCurrentAction。
@RequestMapping(value = "/createEx", produces = "plain/text; charset=UTF-8", method = RequestMethod.POST)
@ResponseBody
public String createEx(@ModelAttribute("SpringWeb") Staff staff, ModelMap model, HttpSession session, HttpServletRequest req) throws CloneNotSupportedException {
if (!canCallCurrentAction(session, BaseAction.EnumUserScope.STAFF.getIndex())) {
logger.debug("无权访问本Action");
return null;
}
(10)注解加配置文件的结合,方便放到生产环境的修改、维护。
(10.1)注解
@Value("${public.account.appid}")
private String PUBLIC_ACCOUNT_APPID;
@Value("${public.account.secret}")
private String PUBLIC_ACCOUNT_SECRET;
(10.2)配置文件
#运行环境
env=LOCAL
#支付服务商公众号APPID
env.appid=
#服务商商户号
env.mchid=
#服务商API密钥
env.secret=
#服务商证书路径
env.cert=
(11)接口方法接收前端的传输数据,使用Model对象接收,设置ModelField类与前端传输数据绑定。
(11.1)接口:staff对象接收前端传输参数。
@RequestMapping(value = "/updateEx", produces = "plain/text; charset=UTF-8", method = RequestMethod.POST)
@ResponseBody
public String updateEx(@ModelAttribute("SpringWeb") Staff staff, ModelMap mm, HttpSession session, HttpServletRequest req) throws CloneNotSupportedException {
if (!canCallCurrentAction(session, BaseAction.EnumUserScope.STAFF.getIndex())) {
logger.debug("无权访问本Action");
return null;
}
(11.2)前端传递name属性。
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="${StaffField.FIELD_NAME_status}" class="staffStatus" lay-filter="spinnerCheck">
<option value="0">在职</option>
<option value="1" disabled="disabled" class="select_status layui-disabled">离职</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label"><strong class="requiredField">*</strong>手机号</label>
<div class="layui-input-inline">
<input type="text" class="layui-input staffPhone" name="${StaffField.FIELD_NAME_phone}" oninput="checkUniqueField(this)" onchange="check_ifDataChange(this,this.value);" autocomplete="off" lay-verify="required|phone" maxlength="11"/>
</div>
</div>
</div>
(12)定义StaffField类,不用hardcode代码。
protected String FIELD_NAME_status;
public String getFIELD_NAME_status() {
return "status";
}
2、BO层
(1)对某些业务逻辑需求进行二次处理(检查权限、传输参数格式),调用dao层接口。
(2)传输格式的检查都是调用model层的checkCRUD方法,方便复用、维护和扩展。
(3)传输给数据库的参数统一调用model层getCrudParam方法,方便复用、维护和扩展。
(4)数据库返回的错误码、错误信息、记录行统一使用lastErrorCode、lastErrorMessage、totalRecord返回给上一层。
(5)检查权限。
检查该用户是否有访问某个存储过程的权限:
/** 检查1个staff有无权限permission */
protected boolean checkStaffPermission(int staffID, String permission) {
if (staffID == SYSTEM) {
return true;
}
StaffPermissionCache spc = (StaffPermissionCache) CacheManager.getCache(DataSourceContextHolder.getDbName(), EnumCacheType.ECT_StaffPermission);
ErrorInfo ecOut = new ErrorInfo();
StaffPermission sp = spc.read1(staffID, permission, ecOut);
if (sp == null) {
lastErrorMessage = "Staff(" + staffID + ")没有权限(" + permission + ")";
logger.debug(lastErrorMessage);
return false;
}
return true;
}
(6)给Action层提供统一的crudObject接口。
public BaseModel createObject(int staffID, int iUseCaseID, BaseModel c) {
return create(staffID, iUseCaseID, c);
}
public List<List<BaseModel>> createObjectEx(int staffID, int iUseCaseID, BaseModel c) {
return createEx(staffID, iUseCaseID, c);
}
public BaseModel updateObject(int staffID, int iUseCaseID, BaseModel c) {
return update(staffID, iUseCaseID, c);
}
public List<List<BaseModel>> updateObjectEx(int staffID, int iUseCaseID, BaseModel c) {
return updateEx(staffID, iUseCaseID, c);
}
public BaseModel retrieve1Object(int staffID, int iUseCaseID, BaseModel c) {
return retrieve1(staffID, iUseCaseID, c);
}
public List<List<BaseModel>> retrieve1ObjectEx(int staffID, int iUseCaseID, BaseModel c) {
return retrieve1Ex(staffID, iUseCaseID, c);
}
public List<?> retrieveNObject(int staffID, int iUseCaseID, BaseModel c) {
return retrieveN(staffID, iUseCaseID, c);
}
public List<List<BaseModel>> retrieveNObjectEx(int staffID, int iUseCaseID, BaseModel c) {
return retrieveNEx(staffID, iUseCaseID, c);
}
public BaseModel deleteObject(int staffID, int iUseCaseID, BaseModel c) {
return delete(staffID, iUseCaseID, c);
}
public List<List<BaseModel>> deleteObjectEx(int staffID, int iUseCaseID, BaseModel c) {
return deleteEx(staffID, iUseCaseID, c);
}
crudObject接口调用内部的crud方法,以updateObject为例做介绍,updateObject调用update方法:
@SuppressWarnings("static-access")
protected BaseModel update(int staffID, int iUseCaseID, BaseModel s) {
checkMapper();
if (!checkUpdatePermission(staffID, iUseCaseID, s)) {
lastErrorCode = EnumErrorCode.EC_NoPermission;
lastErrorMessage = "权限不足";
return null;
}
if (!preCheckUpdate(iUseCaseID, s) || !checkUpdate(iUseCaseID, s)) {
return null;
}
BaseModel ls = null;
try {
lastErrorCode = EnumErrorCode.EC_OtherError;
lastErrorMessage = "";
ls = doUpdate(iUseCaseID, s);
} catch (Exception e) {
if (e.getCause() instanceof SQLNonTransientConnectionException && e.getCause() != null && e.getCause().getMessage().equals("Connection is closed.")
&& ((SQLNonTransientConnectionException) e.getCause()).getSQLState().equals("08003")) {
try {
Thread.currentThread().sleep(MYSQL_ReconnectTimeGap);
ls = doUpdate(iUseCaseID, s);
logger.debug("doUpdate(iUseCaseID, s); OK");
} catch (Exception e2) {
lastErrorCode = EnumErrorCode.EC_OtherError;
lastErrorMessage = e2.getMessage();
return null;
}
} else {
lastErrorCode = EnumErrorCode.EC_OtherError;
lastErrorMessage = e.getMessage();
logger.debug("e.getCause()=" + e.getCause().getMessage() + "\t((SQLNonTransientConnectionException) e.getCause()).getSQLState()=" + ((SQLNonTransientConnectionException) e.getCause()).getSQLState());
return null;
}
}
return ls;
}
(7)传输参数的检查。
Update方法包含传输数据格式的检查,和调用具体操作数据库的方法。
检查mapper:
/** 在运行时,通过本方法,给各BO提供一个有效的mapper */
protected void checkMapper() {
if (mapper == null) {
setMapper();
}
}
检查权限,让子类实现(模板设计模式):
protected boolean checkUpdatePermission(int staffID, int iUseCaseID, BaseModel s) {
throw new RuntimeException("Not yet implemented!");
}
检查对象是否为空:
protected boolean preCheckUpdate(int iUseCaseID, BaseModel s) {
return (s != null);
}
检查传输的参数:
protected boolean checkUpdate(int iUseCaseID, BaseModel s) {
return doCheckUpdate(iUseCaseID, s);
}
protected boolean doCheckUpdate(int iUseCaseID, BaseModel s) {
String err = s.checkUpdate(iUseCaseID);
if (err.length() > 0) {
lastErrorCode = EnumErrorCode.EC_WrongFormatForInputField;
lastErrorMessage = err;
return false;
}
return true;
}
检查传输参数的方法放在Model类里面:
@Override
public String checkUpdate(int iUseCaseID) {
StringBuilder sbError = new StringBuilder();
if (this.printCheckField(field.getFIELD_NAME_ID(), FieldFormat.FIELD_ERROR_ID, sbError) && !FieldFormat.checkID(ID)) //
{
return sbError.toString();
}
if (this.printCheckField(field.getFIELD_NAME_commodityID(), FIELD_ERROR_CommodityID, sbError) && !FieldFormat.checkID(commodityID)) {
return sbError.toString();
}
if (this.printCheckField(field.getFIELD_NAME_barcode(), FIELD_ERROR_barcodes, sbError) && !FieldFormat.checkIfMultiPackagingBarcodes(barcode)) {
return sbError.toString();
}
// int2用于传staffID
if (this.printCheckField(field.getFIELD_NAME_operatorStaffID(), FIELD_ERROR_StaffID, sbError) && !FieldFormat.checkID(operatorStaffID)) //
{
return sbError.toString();
}
return "";
}
以do开头的doUpdate是具体操作数据库的方法,设置了错误码和错误信息,返回数据库信息(用BaseModel或List<BaseModel>接收)。
doUpdate可以让子类实现,满足特定的业务要求:
protected BaseModel doUpdate(int iUseCaseID, BaseModel s) {
Map<String, Object> params = s.getUpdateParam(iUseCaseID, s);
BaseModel bm = mapper.update(params);
lastErrorCode = EnumErrorCode.values()[Integer.parseInt(params.get(BaseAction.SP_OUT_PARAM_iErrorCode).toString())];
lastErrorMessage = params.get(BaseAction.SP_OUT_PARAM_sErrorMsg).toString();
return bm;
}
(8)传输参数的封装。
mapper.update之前,现将参数存放在一个hashmap集合里即s.getUpdateParam(iUseCaseID, s)。
BaseModel的getUpdateParam方法,是未实现的方法,需要子类去实现,编写自己的逻辑:
public Map<String, Object> getUpdateParam(int iUseCaseID, final BaseModel bm) {
throw new RuntimeException("尚未实现getUpdateParam()方法!");
}
例如子类Staff继承了BaseModel,它的实现如下:
// int2标识是否需要返回salt
@Override
public Map<String, Object> getUpdateParam(int iUseCaseID, final BaseModel bm) {
checkParameterInput(bm);
Staff s = (Staff) bm;
Map<String, Object> params = new HashMap<String, Object>();
params.put(field.getFIELD_NAME_returnSalt(), s.getReturnSalt());
switch (iUseCaseID) {
case BaseBO.CASE_ResetMyPassword:
params.put(field.getFIELD_NAME_salt(), s.getSalt() == null ? "" : s.getSalt());
params.put(field.getFIELD_NAME_phone(), s.getPhone() == null ? "" : s.getPhone());
params.put(field.getFIELD_NAME_isFirstTimeLogin(), s.getIsFirstTimeLogin());
break;
case BaseBO.CASE_ResetOtherPassword:
params.put(field.getFIELD_NAME_salt(), s.getSalt() == null ? "" : s.getSalt());
params.put(field.getFIELD_NAME_phone(), s.getPhone() == null ? "" : s.getPhone());
params.put(field.getFIELD_NAME_isFirstTimeLogin(), s.getIsFirstTimeLogin());
break;
case BaseBO.CASE_Staff_Update_OpenidAndUnionid:
params.put(field.getFIELD_NAME_phone(), s.getPhone() == null ? "" : s.getPhone());
params.put(field.getFIELD_NAME_openid(), s.getOpenid() == null ? "" : s.getOpenid());
params.put(field.getFIELD_NAME_unionid(), s.getUnionid() == null ? "" : s.getUnionid());
break;
case BaseBO.CASE_Staff_Update_Unsubscribe:
params.put(field.getFIELD_NAME_ID(), s.getID());
break;
default:
params.put(field.getFIELD_NAME_ID(), s.getID());
params.put(field.getFIELD_NAME_phone(), s.getPhone() == null ? "" : s.getPhone()); // ...== null ?
params.put(field.getFIELD_NAME_name(), s.getName() == null ? "" : s.getName()); // ...== null ?
params.put(field.getFIELD_NAME_ICID(), s.getICID() == "" ? null : s.getICID()); // ...== null ?
params.put(field.getFIELD_NAME_weChat(), s.getWeChat() == "" ? null : s.getWeChat()); // ...== null ?
params.put(field.getFIELD_NAME_passwordExpireDate(), s.getPasswordExpireDate());
params.put(field.getFIELD_NAME_shopID(), s.getShopID());
params.put(field.getFIELD_NAME_departmentID(), s.getDepartmentID());
params.put(field.getFIELD_NAME_roleID(), s.getRoleID()); // RoleID
params.put(field.getFIELD_NAME_status(), s.getStatus());
params.put(field.getFIELD_NAME_isLoginFromPos(), s.getIsLoginFromPos());
break;
}
return params;
}
调用update方法,调用对应mapper.xml里面的select id未update的存储过程SP:
BaseModel bm = mapper.update(params);
<select id="update" statementType="CALLABLE" useCache="false" resultMap="staffMap">
{CALL SP_Staff_Update(
#{iErrorCode, jdbcType=INTEGER, mode=OUT},
#{sErrorMsg, jdbcType=VARCHAR, mode=OUT},
#{ID, mode=IN},
#{name, mode=IN},
#{phone, mode=IN},
#{ICID, mode=IN},
#{weChat, mode=IN},
#{passwordExpireDate, mode=IN},
#{shopID, mode=IN},
#{departmentID, mode=IN},
#{roleID, mode=IN},
#{status, mode=IN},
#{returnSalt,mode=IN}
)}
</select>
综上,BaseBO类具体的职责是检查数据格式、操作数据库、返回错误码、错误信息和数据库信息。
3、Mapper层
数据持久层,用于和数据库进行交互。
操作数据库统一通过调用存储过程CALL SP的方式。
(1)BaseMapper父类定义公共方法。
BaseMapper定义了一些通用的方法供子类使用:
/** 所有的Mapper都是没有状态的,只有操作,所以所有BO共享一个Mapper是没问题的 */
@Component("mapper")
public interface BaseMapper {
public BaseModel create(Map<String, Object> params);
public List<List<BaseModel>> createEx(Map<String, Object> params);
public BaseModel retrieve1(Map<String, Object> params);
public List<List<BaseModel>> retrieve1Ex(Map<String, Object> params);
public List<BaseModel> retrieveN(Map<String, Object> params);
public List<List<BaseModel>> retrieveNEx(Map<String, Object> params);
public List<List<BaseModel>> updateEx(Map<String, Object> params);
public BaseModel update(Map<String, Object> params);
public BaseModel delete(Map<String, Object> params);
public List<List<BaseModel>> deleteEx(Map<String, Object> params);
public void checkUniqueField(Map<String, Object> params);
}
(2)子类扩展自己的方法。
子类可以继承,还有扩展自己的操作数据库的方法:
@Component("staffMapper")
public interface StaffMapper extends BaseMapper {
public Staff resetPassword(Map<String, Object> params);
public Staff updateOpenidAndUnionid(Map<String, Object> params);
public void checkICID(Map<String, Object> params);
public void checkUnionid(Map<String, Object> params);
public void checkWeChat(Map<String, Object> params);
public void checkOpenID(Map<String, Object> params);
public void checkStatus(Map<String, Object> params);
public void checkPhone(Map<String, Object> params);
public void checkIsFirstTimeLogin(Map<String, Object> params);
public void checkName(Map<String, Object> params);
public Staff updateUnsubscribe(Map<String, Object> params);
}
(3)调用存储过程。
这些方法对应StaffMapper.xml里面的select id,调用相应的存储过程:
<select id="create" statementType="CALLABLE" useCache="false" resultMap="staffMap">
{CALL SP_Staff_Create(
#{iErrorCode, jdbcType=INTEGER, mode=OUT},
#{sErrorMsg, jdbcType=VARCHAR, mode=OUT},
#{phone, mode=IN},
#{name, mode=IN},
#{ICID,mode=IN},
#{weChat, mode=IN},
#{salt, mode=IN},
#{passwordExpireDate, mode=IN},
#{isFirstTimeLogin, mode=IN},
#{shopID,mode=IN},
#{departmentID, mode=IN},
#{roleID, mode=IN},
#{status, mode=IN},
#{returnSalt,mode=IN}
)}
</select>
<select id="update" statementType="CALLABLE" useCache="false" resultMap="staffMap">
{CALL SP_Staff_Update(
#{iErrorCode, jdbcType=INTEGER, mode=OUT},
#{sErrorMsg, jdbcType=VARCHAR, mode=OUT},
#{ID, mode=IN},
#{name, mode=IN},
#{phone, mode=IN},
#{ICID, mode=IN},
#{weChat, mode=IN},
#{passwordExpireDate, mode=IN},
#{shopID, mode=IN},
#{departmentID, mode=IN},
#{roleID, mode=IN},
#{status, mode=IN},
#{returnSalt,mode=IN}
)}
</select>
<select id="delete" statementType="CALLABLE" useCache="false" resultMap="staffMap">
{CALL SP_Staff_Delete(
#{iErrorCode, jdbcType=INTEGER, mode=OUT},
#{sErrorMsg, jdbcType=VARCHAR, mode=OUT},
#{ID, mode=IN}
)}
</select>