上一篇介绍了整体架构规划与设计,今天来说一下基类的抽象与封装设计。
技术组件上,使用的是SSM+MyBatisPlus的组合。基于经典三层架构,Controller层负责接收UI请求和响应结果,Service层负责业务逻辑的处理,DAO层负责数据库的读写。
后端内核,设计思想是面向对象。进行抽象,创建基类,通用属性与通用操作由基类统一处理,子类通过继承来扩展属性。个性化操作,子类可以通过新增方法,或覆写基类方法来实现。
首先是实体基类的设计,如下:
package com.huayuan.platform.common.base;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 实体 基类
*
* @author wqliu
* @date 2023-03-06
*/
@Data
public class BaseEntity {
/**
* 标识
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
/**
* 创建人标识
*/
@TableField(value = "create_id", fill = FieldFill.INSERT)
private String createId;
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新人标识
*/
@TableField(value = "update_id", fill = FieldFill.INSERT_UPDATE)
private String updateId;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 版本
*/
@TableField(value = "version", fill = FieldFill.INSERT)
@Version
private Integer version;
/**
* 逻辑删除
*/
@TableField(value = "delete_flag", fill = FieldFill.INSERT)
@TableLogic
private String deleteFlag;
}
首先,定义了无意义标识id,用来做技术标识,采用雪片算法生成。类型设定为字符串而非数值,是因为将来可能会出现数据合并,例如,当前系统要导入某个旧系统的数据,如果采用数值类型,则可能存在主键冲突问题,这时候,使用字符串类型,很容易解决这问题,将旧系统的id加个字母前缀O(old)就可以了。
其次,附加了几个通用属性,创建人、创建时间、修改人、修改时间。这几个字段可以用来做自身系统的数据审计。基于修改时间可以做系统间数据的增量同步。
再次,版本用于做乐观锁,避免并发修改引发的数据错误。逻辑删除标志位用来实现逻辑删除,避免业务数据因为误操作物理删除导致的数据丢失,也利于系统异常排查阶段定位原因。
上面几个字段的抽取与设计,设计上是基于经验,技术上是使用MybatisPlus提供的功能完成。id的自动生成,创建人、创建时间、修改人、修改时间、版本号和逻辑删除位自动填充,业务开发过程中不需要额外处理。
所有业务上的实体类,都继承自此基类 ,一方面保证了整体上的一致,后续进行通用功能设计易于实现;另一方面,后续有扩展需求了,则可以直接扩展基类,同样很轻松。
由于MybatisPlus对于该层已经做了很好的封装和处理,因此没有再扩展,直接使用包com.baomidou.mybatisplus.core.mapper下的BaseMapper作为基类,重点放在下面的业务逻辑
业务逻辑层,也就是通常所说的Service层,会定义一个接口类,一个实现类。MybatisPlus提供了功能强大的基类,我这里进行了扩展,使用模板方法的理念,将通用操作集中到基类中处理,子类仅需要处理具体的业务需求就行了。
首先是服务接口基类,继承自mybatisplus封装的基类IService,定义了初始化、增删改查的方法,以及批量操作。
package com.huayuan.platform.common.base;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* 服务接口,定义通用操作
*
* @author wqliu
* @date 2023-03-06
*/
public interface BaseService<T> extends IService<T> {
/**
* 初始化
*
* @return 实体对象
*/
T init();
/**
* 新增
*
* @param entity 实体对象
* @return ture:成功;false:失败
*/
boolean add(T entity);
/**
* 修改
*
* @param entity 实体对象
* @return ture:成功;false:失败
*/
boolean modify(T entity);
/**
* 删除-通过id
*
* @param idListString id列表字符串,多个id用逗号间隔
* @return
*/
boolean remove(String idListString);
/**
* 批量新增
*
* @param list 实体列表
* @return
*/
boolean batchAdd(List<T> list);
/**
* 批量修改
*
* @param list 实体列表
* @return
*/
boolean batchModify(List<T> list);
/**
* 删除-通过条件表达式
* 注意,删除大量数据会导致性能问题
*
* @param wrapper
* @return
*/
@Override
boolean remove(Wrapper<T> wrapper);
/**
* 查询
*
* @param id 标识
* @return
*/
T query(String id);
}
然后是服务实现基类,继承自mybatisplus封装的基类BaseService,实现了我扩展定义的服务接口。
package com.huayuan.platform.common.base;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.huayuan.platform.common.exception.CommonException;
import com.huayuan.platform.common.exception.CustomException;
import com.huayuan.platform.common.utils.CacheUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 服务类基类,处理通用操作
*
* @author wqliu
*/
public class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseEntity>
extends ServiceImpl<M, T> implements BaseService<T> {
@Autowired
protected CacheUtil cacheUtil;
@Override
public T init() {
return null;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean add(T entity) {
beforeAdd(entity);
beforeAddOrModifyOp(entity);
boolean result = super.save(entity);
afterAddOrModifyOp(entity);
afterAdd(entity);
return result;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean modify(T entity) {
beforeModify(entity);
beforeAddOrModifyOp(entity);
if (entity instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) entity;
baseEntity.setUpdateId(null);
baseEntity.setUpdateTime(null);
}
boolean result = super.updateById(entity);
afterAddOrModifyOp(entity);
afterModify(entity);
return result;
}
/**
* 删除-通过id
*
* @param idListString id列表字符串,多个id用逗号间隔
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean remove(String idListString) {
String[] idArray = StringUtils.split(idListString.toString(), ",");
for (String item : idArray) {
T entity = getEntity(item);
beforeRemove(entity);
super.removeById(item);
afterRemove(entity);
}
return true;
}
@Override
public T query(String id) {
beforeQuery(id);
T result = getById(id);
// 对象非空判断
if (result == null) {
throw new CustomException(CommonException.NOT_EXIST);
}
afterQuery(result);
return result;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean batchAdd(List<T> list) {
for (T entity : list) {
add(entity);
}
return true;
}
@Override
public boolean batchModify(List<T> list) {
for (T entity : list) {
modify(entity);
}
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean remove(Wrapper<T> wrapper) {
List<T> boList = super.list(wrapper);
for (T entity : boList) {
beforeRemove(entity);
}
boolean result = super.remove(wrapper);
for (T entity : boList) {
afterRemove(entity);
}
return result;
}
/**
* 新增前
*
* @param entity 实体
*/
protected void beforeAdd(T entity) {
// 子类根据需要覆写
}
/**
* 新增后
*
* @param entity 实体
*/
protected void afterAdd(T entity) {
// 子类根据需要覆写
}
/**
* 修改前
*
* @param entity 实体
*/
protected void beforeModify(T entity) {
// 子类根据需要覆写
}
/**
* 修改后
*
* @param entity 实体
*/
protected void afterModify(T entity) {
// 子类根据需要覆写
}
/**
* 新增和修改前公用操作
*/
protected void beforeAddOrModifyOp(T entity) {
// 子类根据需要覆写
}
/**
* 新增和修改后公用操作
*/
protected void afterAddOrModifyOp(T entity) {
// 子类根据需要覆写
}
/**
* 删除前
*
* @param entity 实体
*/
protected void beforeRemove(T entity) {
// 子类根据需要覆写
}
/**
* 删除后
*
* @param entity 实体
*/
protected void afterRemove(T entity) {
// 子类根据需要覆写
}
/**
* 查询前
*
* @param id id
*/
protected void beforeQuery(String id) {
// 子类根据需要覆写
}
/**
* 查询后
*
* @param entity 实体
*/
protected void afterQuery(T entity) {
// 子类根据需要覆写
}
/**
* 获取实体
*
* @param id id
* @return {@link T}
*/
protected T getEntity(String id) {
// 标识非空判断
if (id == null) {
throw new CustomException(CommonException.ID_EMPTY);
}
T entity = query(id);
return entity;
}
}
对业务实体的增删改查,通常有前置操作和后置操作,并且需要事务处理。
以新增操作为例,通过基类来定义算法的框架,在add方法中增加事务注解,如下:
@Transactional(rollbackFor = Exception.class)
@Override
public boolean add(T entity) {
beforeAdd(entity);
beforeAddOrModifyOp(entity);
boolean result = super.save(entity);
afterAddOrModifyOp(entity);
afterAdd(entity);
return result;
}
protected void beforeAdd(T entity) {
//子类根据需要覆写
}
protected void afterAdd(T entity) {
//子类根据需要覆写
}
子类根据需要覆写
@Override
public void beforeAdd(Organization entity) {
//验证同节点下是否存在名称相同的组织机构
QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Organization::getName, entity.getName())
.eq(Organization::getParentId, entity.getParentId());
int count = count(queryWrapper);
if (count > 0) {
throw new CustomException(ExceptionEnum.ORGANIZATION_NAME_EXIST);
}
}
@Override
public void afterAdd(Organization entity) {
//将ID与名称放到redis缓存
cacheUtil.set(Constant.ORGANIZATION_CACHE_PREFIX + entity.getId(), entity.getName());
}
注意:该实例与标准的模板方法还有一些差异,基类并没有将beforeAdd和afterAdd两个方法定义为抽象方法。原因在于这两个处理环节对于具体的业务实体并不是必须的,如定义为抽象方法,则会要求子类必须实现,反而不符合实际需求并带来繁琐性。
对于设计模式,很多都在标准模式下有变种和变通,我们应该领会其神,活学活用。
通过上述封装后,业务实体通用的增删改查都已经实现了。在进行具体业务系统的功能开发时,仅需要继承该父类,实现特定的业务逻辑即可,代码量大幅减少,代码简洁直观,易于维护,以组织机构为例。
服务接口类,继承基类后,仅需定义三个个性化服务接口即可。
package com.huayuan.platform.system.service;
import com.huayuan.platform.common.base.BaseService;
import com.huayuan.platform.system.entity.Organization;
import java.util.List;
/**
* 组织机构 服务类
*
* @author wqliu
* @date 2023-03-06
*/
public interface OrganizationService extends BaseService<Organization> {
/**
* 启用
*
* @param id 标识
*/
void enable(String id);
/**
* 停用
*
* @param id 标识
*/
void disable(String id);
/**
* 获取当前组织机构所有上级(包括自身)
*
* @param organizationId 组织机构标识
* @return 组织机构列表
*/
List<String> getParentId(String organizationId);
}
服务实现类,继承基类后,除了实现个性化的服务接口外,仅根据实际需要,覆写基类预留的方法,如通过beforeAdd,验证同一组织机构下是否已存在同名的记录,通过beforeRemove,验证是否存在下级组织机构,以及该组织机构下是否存在用户,通过afterAdd等方法,更新缓存数据。
package com.huayuan.platform.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.huayuan.platform.common.base.BaseServiceImpl;
import com.huayuan.platform.common.constant.CacheConstant;
import com.huayuan.platform.common.constant.TreeDefaultConstant;
import com.huayuan.platform.common.enums.StatusEnum;
import com.huayuan.platform.common.exception.CommonException;
import com.huayuan.platform.common.exception.CustomException;
import com.huayuan.platform.common.utils.CacheUtil;
import com.huayuan.platform.system.entity.Organization;
import com.huayuan.platform.system.entity.User;
import com.huayuan.platform.system.exception.OrganizationExceptionEnum;
import com.huayuan.platform.system.mapper.OrganizationMapper;
import com.huayuan.platform.system.service.DictionaryTypeService;
import com.huayuan.platform.system.service.OrganizationService;
import com.huayuan.platform.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.Validator;
import java.util.ArrayList;
import java.util.List;
/**
* 组织机构 服务实现类
*
* @author wqliu
* @since 2023-03-06
*/
@Service
@Slf4j
public class OrganizationServiceImpl extends BaseServiceImpl<OrganizationMapper, Organization>
implements OrganizationService {
@Autowired
private UserService userService;
@Autowired
private DictionaryTypeService dictionaryTypeService;
@Autowired
private CacheUtil cacheUtil;
@Autowired
private Validator validator;
@Override
public Organization init() {
Organization entity = new Organization();
entity.setStatus(StatusEnum.NORMAL.name());
return entity;
}
@Override
public void beforeAdd(Organization entity) {
// 验证同节点下是否存在同名节点
QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Organization::getName, entity.getName())
.eq(Organization::getParentId, entity.getParentId());
long count = count(queryWrapper);
if (count > 0) {
throw new CustomException(CommonException.NAME_EXIST_IN_SAME_NODE);
}
}
@Override
public void beforeModify(Organization entity) {
// 父节点不能为自己
if (entity.getId().equals(entity.getParentId())) {
throw new CustomException(CommonException.UP_CANNOT_SELF);
}
// 所选父节点不能为子节点
if (this.hasChild(entity.getId(), entity.getParentId())) {
throw new CustomException(CommonException.UP_CANNOT_BE_CHILD);
}
// 验证同节点下是否存在名称相同的组织机构
QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Organization::getName, entity.getName())
.eq(Organization::getParentId, entity.getParentId())
.ne(Organization::getId, entity.getId());
long count = count(queryWrapper);
if (count > 0) {
throw new CustomException(CommonException.NAME_EXIST_IN_SAME_NODE);
}
}
@Override
public void beforeRemove(Organization entity) {
// 验证是否有下级
if (super.lambdaQuery().eq(Organization::getParentId, entity.getId()).count() > 0) {
throw new CustomException(CommonException.HAS_CHILDREN);
}
// 验证是否存在人员
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getOrganizationId, entity.getId());
long count = userService.count(queryWrapper);
if (count > 0) {
throw new CustomException(OrganizationExceptionEnum.HAS_USER);
}
}
@Override
public void afterAdd(Organization entity) {
cacheUtil.set(CacheConstant.ORGANIZATION_CACHE_PREFIX + entity.getId(), entity.getName());
}
@Override
public void afterModify(Organization entity) {
cacheUtil.set(CacheConstant.ORGANIZATION_CACHE_PREFIX + entity.getId(), entity.getName());
}
@Override
public void afterRemove(Organization entity) {
cacheUtil.remove(CacheConstant.ORGANIZATION_CACHE_PREFIX + entity.getId());
}
/**
* 判断选中的树节点与变化树节点的父子关系
*
* @param selectId
* @param changeId
* @return
*/
private Boolean hasChild(String selectId, String changeId) {
// 默认不为子节点,有子节点则停止递归查询
Boolean ret = false;
QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Organization::getParentId, selectId);
List<Organization> childList = this.list(queryWrapper);
if (childList != null) {
for (Organization o : childList) {
if (changeId.equals(o.getId())) {
return true;
}
ret = hasChild(o.getId(), changeId);
if (ret) {
return ret;
}
}
}
return ret;
}
@Override
public void enable(String id) {
Organization entity = query(id);
entity.setStatus(StatusEnum.NORMAL.name());
modify(entity);
}
@Override
public void disable(String id) {
Organization entity = query(id);
entity.setStatus(StatusEnum.DEAD.name());
modify(entity);
}
@Override
public List<String> getParentId(String organizationId) {
List<String> list = new ArrayList<>(10);
Organization entity = null;
do {
// 获取当前实体
entity = getEntity(organizationId);
// 添加到集合
list.add(organizationId);
// 将当前实体父标识设置为实体标识
organizationId = entity.getParentId();
}
while (!TreeDefaultConstant.DEFAULT_TREE_ROOT_PARENT_ID.equals(organizationId));
return list;
}
}
控制器层进行的封装比较少,主要是考虑到不同的业务实体会存在差异化需求,例如,某个业务实体不允许修改或删除,那么在controller层就不应当暴露修改或删除的rest api。
控制器层仍有共性部分,将分页与排序对象的绑定,以及工具类的定义,放到基类统一处理,子类直接使用即可,具体如下:
package com.huayuan.platform.common.base;
import com.huayuan.platform.common.query.QueryGenerator;
import com.huayuan.platform.common.utils.CacheUtil;
import com.huayuan.platform.common.utils.DictionaryUtil;
import ma.glasnost.orika.MapperFacade;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
/**
* 控制器基类
*
* @author wqliu
* @date 2023-03-06
*/
public class BaseController {
@Autowired
protected MapperFacade mapperFacade;
@Autowired
protected QueryGenerator queryGenerator;
@Autowired
protected DictionaryUtil dictionaryUtil;
@Autowired
protected CacheUtil cacheUtil;
/**
* 分页
*
* @param binder
*/
@InitBinder("pageInfo")
public void initPageInfo(WebDataBinder binder) {
binder.setFieldDefaultPrefix("page_");
}
/**
* 排序
*
* @param binder
*/
@InitBinder("sortInfo")
public void initSort(WebDataBinder binder) {
binder.setFieldDefaultPrefix("sort_");
}
}
视图对象的基类保持与实体基类一致,具体如下:
package com.huayuan.platform.common.base;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 视图对象 基类
*
* @author wqliu
* @date 2023-03-06
*/
@Data
public class BaseVO {
/**
* 标识
*/
private String id;
/**
* 创建人标识
*/
private String createId;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新人标识
*/
private String updateId;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 版本
*/
private Integer version;
/**
* 删除标志
*/
private String deleteFlag;
}
这里也顺便提一下,为什么要有视图对象(vo)?直接使用实体对象(entity)行不行?
在前后端分离模式下,通过json交换数据,视图对象非常重要。引入视图对象,相对于直接使用实体对象,有以下优点:
1.可限制前端能输入的数据,如某个属性不允许作为查询条件输入;
2.可减少返回给前端的数据,如实体有30个属性,返回给前端只有10个属性,可屏蔽不希望提供给前端展示的敏感数据,如用户密码,商品采购成本等
3.可增加属性数量,在controller层,将业务实体的字典编码,转换为字典名称,输出给前端,需要额外属性承载。
4.可进行数据聚合,将来源于几个entity的属性聚合成一份供前端使用的数据
5.前后端解耦,前端更换UI控件库,如将ElementUI更换为Eelement Plus、IView或AntD of Vue,有vo层做缓冲将大幅降低需要改造和适配的工作量。这里指的不是业务实体对应的vo,而是与前端UI控件库对应的vo,如不同的UI库,树的数据结构不同。
平台设计与设计专栏地址:https://blog.csdn.net/seawaving/category_12230971.html
开源项目地址:https://gitee.com/popsoft/huayuan-development-platform
欢迎收藏、点赞、评论。