前几张,我们主要实现了升级经验、人物等级属性、地图、地图怪物,这四种配置的增删查改以及Excel导入功能。我们主要以地图怪物为例,因此在文章末尾提供的源代码中只实现了地图怪物这部分的逻辑功能。
如果你照猫画虎,把4种配置功能的逻辑全部实现的话,就会发现,增删查改的代码基本相同,除了SQL语句和模型对象不同,其他地方变化不大。
本章我们利用泛型模板,对整个系统就行重构。在重构结束后,你就会发现写代码简直就是特喵的艺术!
后端重构
idlewow-core
我们从最底层开始,首先重构位于core模块中的数据访问层。目前看来,基本上所有的模型对象,都应包含增删查改、批量添加、列表查询这些基本方法。那我们把这些方法抽象到一个单独的Mapper和Manager里。
新建com.idlewow.common包,再该包下新建接口类BaseMapper:
packagecom.idlewow.common;importcom.idlewow.common.model.QueryParam;importjava.util.List;public interface BaseMapper{/*** 添加记录
*@paramt*/
intinsert(T t);/*** 批量添加记录
*@paramlist
*@return
*/
int batchInsert(Listlist);/*** 更新记录
*@paramt*/
intupdate(T t);/*** 删除记录
*@paramid*/
intdelete(String id);/*** 根据id查询
*@paramid
*@return
*/T find(String id);/*** 根据条件查询总数
*@paramqueryParam
*@return
*/
intcount(QueryParam queryParam);/*** 根据条件查询列表
*@paramqueryParam
*@return
*/Listlist(QueryParam queryParam);
}
BaseMapper.java
再在该包下,新建一个抽象类BaseManager,代码如下:
packagecom.idlewow.common;importcom.idlewow.common.model.PageList;importcom.idlewow.common.model.QueryParam;importorg.springframework.beans.factory.annotation.Autowired;importjava.util.List;public abstract class BaseManager{
@Autowiredpublic BaseMapperbaseMapper;public voidinsert(T t) {int effected =baseMapper.insert(t);if (effected == 0) {throw new RuntimeException("sql effected 0 rows");
}
}public void batchInsert(Listlist) {int splitSize = 100;int index = 0;int total =list.size();while (index <=total) {int end = index +splitSize;if (end >total) {
end=total;
}
List sublist =list.subList(index, end);int effected =baseMapper.batchInsert(sublist);if (effected == 0) {throw new RuntimeException("sql effected 0 rows");
}
index+=splitSize;
}
}public voidupdate(T t) {int effected =baseMapper.update(t);if (effected == 0) {throw new RuntimeException("sql effected 0 rows");
}
}public voiddelete(String id) {int effected =baseMapper.delete(id);if (effected == 0) {throw new RuntimeException("sql effected 0 rows");
}
}publicT find(String id) {
T t=(T)baseMapper.find(id);returnt;
}public PageListlist(QueryParam queryParam) {
PageList pageList = new PageList<>();int count =baseMapper.count(queryParam);
List list =baseMapper.list(queryParam);
pageList.setTotalCount(count);
pageList.setData(list);
pageList.setPageParam(queryParam.getPageParam());returnpageList;
}
}
BaseManager.java
还是以地图怪物为例,我们重构MapMobMapper和MapMobManager,只要让他们继承BaseMapper和BaseManager即可,代码如下:
packagecom.idlewow.mob.mapper;importcom.idlewow.common.BaseMapper;importcom.idlewow.mob.model.MapMob;public interface MapMobMapper extends BaseMapper{
}
MapMobMapper.java
packagecom.idlewow.mob.manager;importcom.idlewow.common.BaseManager;importcom.idlewow.mob.model.MapMob;importorg.springframework.stereotype.Component;
@Componentpublic class MapMobManager extends BaseManager{
}
MapMobManager.java
重构后的Mapper和Manager直接继承基类的增删查改方法,无需在各个业务中一遍又一遍的书写重复代码。
idlewow-rms
在rms模块中,主要对controller中的重复代码进行重构。在前几章,我们抽象出过一个BaseController,在里面实现了一些最基础的方法。这里,我们再抽象出一个CrudController来实现数据的增删查该;在CrudController的基础上,再抽象出一个ExcelController来实现Excel的批量导入。
在com.idlewow.rms.controlelr包下新建抽象类CrudContoller,代码如下:
packagecom.idlewow.rms.controller;importcom.idlewow.common.BaseManager;importcom.idlewow.common.model.BaseModel;importcom.idlewow.common.model.CommonResult;importcom.idlewow.common.model.PageList;importcom.idlewow.common.model.QueryParam;importcom.idlewow.util.validation.ValidateGroup;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.ui.Model;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.ResponseBody;public abstract class CrudController extendsBaseController {private final String path = this.getClass().getAnnotation(RequestMapping.class).value()[0];
@Autowired
BaseManagerbaseManager;
@RequestMapping("/list")publicObject list() {return this.path + "/list";
}
@ResponseBody
@RequestMapping(value= "/list", method =RequestMethod.POST)public Object list(@RequestParam(value = "page", defaultValue = "1") int pageIndex, @RequestParam(value = "limit", defaultValue = "10") intpageSize, Q q) {
q.setPage(pageIndex, pageSize);
PageList pageList =baseManager.list(q);return this.parseTable(pageList);
}
@RequestMapping("/add")publicObject add() {return this.path + "/add";
}
@ResponseBody
@RequestMapping(value= "/add", method =RequestMethod.POST)publicObject add(@RequestBody T t) {try{
CommonResult commonResult= this.validate(t, ValidateGroup.Create.class);if (!commonResult.isSuccess())returncommonResult;
t.setCreateUser(this.currentUserName());
baseManager.insert(t);returnCommonResult.success();
}catch(Exception ex) {
logger.error(ex.getMessage(), ex);returnCommonResult.fail();
}
}
@RequestMapping(value= "/edit/{id}", method =RequestMethod.GET)publicObject edit(@PathVariable String id, Model model) {
T t=baseManager.find(id);
model.addAttribute(t);return this.path + "/edit";
}
@ResponseBody
@RequestMapping(value= "/edit/{id}", method =RequestMethod.POST)publicObject edit(@PathVariable String id, @RequestBody T t) {try{if (!id.equals(t.getId())) {return CommonResult.fail("id不一致");
}
CommonResult commonResult= this.validate(t, ValidateGroup.Update.class);if (!commonResult.isSuccess())returncommonResult;
t.setUpdateUser(this.currentUserName());
baseManager.update(t);returnCommonResult.success();
}catch(Exception ex) {
logger.error(ex.getMessage(), ex);returnCommonResult.fail();
}
}
@ResponseBody
@RequestMapping(value= "/delete/{id}", method =RequestMethod.POST)publicObject delete(@PathVariable String id) {try{
baseManager.delete(id);returnCommonResult.success();
}catch(Exception ex) {
logger.error(ex.getMessage(), ex);returnCommonResult.fail();
}
}
}
CrudController.java
这个CrudController起到了一个模板的作用,可以说非常的精髓。首先,利用泛型约束,解决了不同业务数据模型、查询参数不同的问题。然后,通过反射获取不同业务controller的Url映射,解决了不同业务跳转页面路径不同的问题。具体业务的controller直接继承此类,无需再写任何代码,即可实现增删查改。
下面我们再实现Excel导入功能的模板类,在该包内新建一个抽象类ExcelController,代码如下:
packagecom.idlewow.rms.controller;importcom.idlewow.common.model.BaseModel;importcom.idlewow.common.model.CommonResult;importcom.idlewow.common.model.QueryParam;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.bind.annotation.ResponseBody;importorg.springframework.web.multipart.MultipartFile;importorg.springframework.web.multipart.MultipartHttpServletRequest;importorg.springframework.web.multipart.commons.CommonsMultipartResolver;importjavax.servlet.ServletContext;importjavax.servlet.http.HttpServletRequest;importjava.io.File;importjava.util.Iterator;importjava.util.List;public abstract class ExcelController extends CrudController{
@ResponseBody
@RequestMapping(value= "/importExcel", method =RequestMethod.POST)publicObject importExcel(HttpServletRequest request) {try{
ServletContext servletContext=request.getServletContext();
String uploadPath= servletContext.getRealPath("/upload");
File dir= newFile(uploadPath);if (!dir.exists()) {
dir.mkdir();
}
CommonsMultipartResolver multipartResolver= newCommonsMultipartResolver(servletContext);if(multipartResolver.isMultipart(request)) {
MultipartHttpServletRequest multiRequest=(MultipartHttpServletRequest) request;
Iterator iter =multiRequest.getFileNames();while(iter.hasNext()) {
MultipartFile file=multiRequest.getFile(iter.next());if (file.getSize() > 0) {
String fileName=file.getOriginalFilename();
String extension= fileName.substring(fileName.lastIndexOf("."));if (!extension.toLowerCase().equals(".xls") && !extension.toLowerCase().equals(".xlsx")) {throw new Exception("不支持的文档格式!请上传.xls或.xlsx格式的文档!");
}
String destFileName= fileName + "_" + System.currentTimeMillis() +extension;
File destFile= newFile(uploadPath, destFileName);
file.transferTo(destFile);
List dataList = this.loadExcelData(destFile.getPath());this.saveExcelData(dataList);if (destFile.exists() && !destFile.delete()) {
logger.error("删除临时文件失败!" +destFile.getAbsolutePath());
}
}
}
}returnCommonResult.success();
}catch(Exception ex) {
logger.error(ex.getMessage(), ex);returnCommonResult.fail();
}
}protected abstract List loadExcelData(String excelPath) throwsException;protected void saveExcelData(ListdataList) {this.baseManager.batchInsert(dataList);
}
}
ExcelController.java
在这个类中,我们将Excel导入功能分解成3个方法。importExcel,对应前端点击事件,保存上传的临时文件;saveExcelData,保存解析出的数据,即调用mapper的批量添加方法;这两个方法都是通用的,直接在ExcelController中实现即可。只有loadExcelData,解析Excel数据这个方法,不同业务的实现不同,我们把它定义成抽象方法,等待各个业务自己实现。
好了,基类已经定义好了,我们让MapMobController继承ExcelController即可,代码如下:
packagecom.idlewow.rms.controller;importcom.idlewow.common.constant.DataDict;importcom.idlewow.map.model.WowMap;importcom.idlewow.mob.model.MapMob;importcom.idlewow.query.model.MapMobQueryParam;importcom.idlewow.util.poi.PoiUtil;importorg.apache.poi.ss.usermodel.Sheet;importorg.apache.poi.xssf.usermodel.XSSFRow;importorg.apache.poi.xssf.usermodel.XSSFWorkbook;importorg.ehcache.Cache;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importjava.io.FileInputStream;importjava.util.ArrayList;importjava.util.List;
@Controller
@RequestMapping("/manage/map_mob")public class MapMobController extends ExcelController{
@AutowiredprotectedCache mapCache;protected List loadExcelData(String excelPath) throwsException {
FileInputStream fileInputStream= newFileInputStream(excelPath);
XSSFWorkbook workbook= newXSSFWorkbook(fileInputStream);
Sheet sheet= workbook.getSheet("怪物");
List mapMobList = new ArrayList<>();//处理当前页,循环读取每一行
String createUser = this.currentUserName();for (int rowNum = 2; rowNum <= sheet.getLastRowNum(); rowNum++) {
XSSFRow row=(XSSFRow) sheet.getRow(rowNum);
String mapName= PoiUtil.getCellValue(row.getCell(1));
String mobName= PoiUtil.getCellValue(row.getCell(2));
String faction= PoiUtil.getCellValue(row.getCell(3));
String mobClass= PoiUtil.getCellValue(row.getCell(4));
String mobType= PoiUtil.getCellValue(row.getCell(5));
Integer level= Integer.valueOf(PoiUtil.getCellValue(row.getCell(6)));
Integer hp= Integer.valueOf(PoiUtil.getCellValue(row.getCell(7)));
Integer damage= Integer.valueOf(PoiUtil.getCellValue(row.getCell(8)));
Integer amour= Integer.valueOf(PoiUtil.getCellValue(row.getCell(9)));
WowMap wowMap=(WowMap)mapCache.get(mapName);
MapMob mapMob= newMapMob();
mapMob.setMapId(wowMap.getId());
mapMob.setMapName(wowMap.getName());
mapMob.setName(mobName);
mapMob.setFaction(DataDict.Faction.getByDesc(faction).getCode());
mapMob.setMobClass(DataDict.MobClass.getByDesc(mobClass).getCode());
mapMob.setMobType(DataDict.MobType.getByDesc(mobType).getCode());
mapMob.setLevel(level);
mapMob.setHp(hp);
mapMob.setDamage(damage);
mapMob.setAmour(amour);
mapMob.setCreateUser(createUser);
mapMobList.add(mapMob);
}
fileInputStream.close();returnmapMobList;
}
}
MapMobController.java
现在业务Controller里,只有一个独立实现的loadExcelData方法,再也不用重复书写增删查改了。(注意:这个类里多了个缓存对象mapCache,是我用来缓存地图数据的。pom中添加了对应包,具体可在源码中查看。用法比较简单,可搜EhCache。)
前端重构
除了后端代码冗余外,其实很容易发现,前端的代码重复部分也很多,尤其是写ajax请求的部分,其实每次变化的只有url地址,或者请求参数等。我们直接在js里定义一个类来实现增删查改等这些ajax请求的通用部分。
在/webapp/js/helper.js中,定义一个类CRUD,代码如下:
…………
…………var CRUD = function() {
};
CRUD.prototype={
list:function(cols, url) {var table =layui.table;
table.render({
elem:'#datatable', url: url
, method:'post', cellMinWidth:80, cols: cols
, page: {
layout: ['limit', 'count', 'prev', 'page', 'next', 'skip'] //自定义分页布局
, limits: [10, 20, 30, 40, 50]
, groups:3 //只显示 1 个连续页码
, first: '首页', last:'尾页'}
});
},
upload:function(url, extension) {
layui.upload.render({
elem:'#btnSelectFile',
url: url,
accept:'file',
exts: extension,
auto:false,
bindAction:'#btnImport',
done:function(result) {if (result.code === 1) {
layer.alert(result.message, {icon:6},function() {
layui.layer.closeAll();
layui.table.reload('datatable');
});
}else{
layer.alert(result.message, {icon:5});
}
}
});
},
search:function(data) {var table =layui.table;
table.reload('datatable', {
where: data,
page: {
curr:1}
});
},
add:function(url) {var form =layui.form;
form.on('submit(add)',function(data) {
$.ajax({
url: url,
type:'post',
contentType:"application/json; charset=utf-8",
data: JSON.stringify(data.field),
success:function(result) {if (result.code === 1) {
layer.alert(result.message, {icon:6},function() {
xadmin.close();
xadmin.father_reload();
});
}else{
layer.alert(result.message, {icon:5});
}
},
error:function() {
layer.alert("请求失败", {icon: 5});
}
});
});
},
edit:function(url) {var form =layui.form;
form.on('submit(edit)',function(data) {
$.ajax({
url: url+ '/' +data.field.id,
type:'post',
contentType:"application/json; charset=utf-8",
data: JSON.stringify(data.field),
success:function(result) {if (result.code === 1) {
layer.alert(result.message, {icon:6},function() {
xadmin.close();
xadmin.father_reload();
});
}else{
layer.alert(result.message, {icon:5});
}
},
error:function() {
layer.alert("请求失败", {icon: 5});
}
});
});
},
remove:function(obj, url) {
layer.confirm('确认要删除吗?', function() {
$.ajax({
url: url,
type:'post',
success:function(result) {if (result.code === 1) {
$(obj).parents("tr").remove();
layer.msg('删除成功', {icon: 1, time: 1000});
}else{
layer.alert("删除失败", {icon: 5});
}
},
error:function() {
layer.alert("请求失败", {icon: 5});
}
});
});
}
};
window.crud= newCRUD();
…………
…………
Helper.js
然后,在/webapp/js/wow/map_mob中,修改add.js, edit.js 和 list.js 如下:
layui.use(['form', 'layer'],function() {var form =layui.form;
form.verify({});
crud.add('/manage/map_mob/add');
});
add.js
layui.use(['form', 'layer'],function() {var form =layui.form;
form.render();
form.verify({});
crud.edit('/manage/map_mob/edit/');
});
edit.js
layui.use(['upload', 'table', 'form'], function() {var cols =[[
{field:'id', width: 50, title: 'id'}
, {field:'name', title: '怪物名称'}
, {field:'mapName', title: '地图名称'}
, {
field:'faction', title: '阵营', templet: function(d) {returnenumUtil.faction(d.faction);
}
}
, {
field:'mobClass', title: '怪物种类', templet: function(d) {returnenumUtil.mobClass(d.mobClass);
}
}
, {
field:'mobType', title: '怪物类型', templet: function(d) {returnenumUtil.mobType(d.mobType);
}
}
, {field:'level', title: '等级'}
, {field:'hp', title: '生命值'}
, {field:'damage', title: '伤害'}
, {field:'amour', title: '护甲'}
, {
title:'操作', width: 150, templet: function(d) {return '编辑' +
'删除';
}
}
]];
crud.list(cols,'/manage/map_mob/list');
crud.upload('/manage/map_mob/importExcel', 'xls|xlsx');
});functionsearch() {var data ={
name: $('input[name="name"]').val(),
levelStart: $('input[name="levelStart"]').val(),
levelEnd: $('input[name="levelEnd"]').val(),
faction: $('select[name="faction"]').val(),
mobClass: $('select[name="mobClass"]').val(),
mobType: $('select[name="mobType"]').val()
};
crud.search(data);
}functionreset(){
$('#queryForm').reset();
}functionremove(obj, id) {
crud.remove(obj,'/manage/map_mob/delete/' +id);
}
list.js
可以看到,重构后的js,和后端一样,简洁多了。
运行效果
这里,由于改动了core模块,需要先对项目编译打包。再运行rms模块,即可正常启动项目。启动后,效果和之前一样,只是代码变得简洁多了,这里就不再截图了。
小结
本章终于把冗余的代码进行了重构,整个代码瞬间提升了几层逼格,变得干净多了。
说明:我的代码风格就是不套用设计模式,在不确定最终效果时,不做过多提前设计,先实现了再说,实现了再慢慢重构,还有就是几乎不注释。
最近因为有其他的事情,所以停更了一周。写到这里其实发现这一大章题目似乎叫RMS系统的初步实现更好。
写代码和写文章其实差别还有点大。自己写代码的时候,想到哪写到哪,各个模块并行随缘开发。但写文章就必须按一定顺序来,否则容易让人困惑。所以文章里的代码都是单独拉一个分支重新整理的。
项目交流群:329989095