开发平台后端基类的抽象与封装设计
创始人
2025-06-01 06:17:11

上一篇介绍了整体架构规划与设计,今天来说一下基类的抽象与封装设计。

技术组件上,使用的是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)@Versionprivate Integer version;/*** 逻辑删除*/@TableField(value = "delete_flag", fill = FieldFill.INSERT)@TableLogicprivate 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 extends IService {/*** 初始化** @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 list);/*** 批量修改** @param list 实体列表* @return*/boolean batchModify(List list);/*** 删除-通过条件表达式* 注意,删除大量数据会导致性能问题** @param wrapper* @return*/@Overrideboolean remove(Wrapper 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, T extends BaseEntity>extends ServiceImpl implements BaseService {@Autowiredprotected CacheUtil cacheUtil;@Overridepublic T init() {return null;}@Transactional(rollbackFor = Exception.class)@Overridepublic boolean add(T entity) {beforeAdd(entity);beforeAddOrModifyOp(entity);boolean result = super.save(entity);afterAddOrModifyOp(entity);afterAdd(entity);return result;}@Transactional(rollbackFor = Exception.class)@Overridepublic 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)@Overridepublic 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;}@Overridepublic 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)@Overridepublic boolean batchAdd(List list) {for (T entity : list) {add(entity);}return true;}@Overridepublic boolean batchModify(List list) {for (T entity : list) {modify(entity);}return true;}@Transactional(rollbackFor = Exception.class)@Overridepublic boolean remove(Wrapper wrapper) {List 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)@Overridepublic 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) {//子类根据需要覆写}

子类根据需要覆写

@Overridepublic void beforeAdd(Organization entity) {//验证同节点下是否存在名称相同的组织机构QueryWrapper 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);}}@Overridepublic 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 {/*** 启用** @param id 标识*/void enable(String id);/*** 停用** @param id 标识*/void disable(String id);/*** 获取当前组织机构所有上级(包括自身)** @param organizationId 组织机构标识* @return 组织机构列表*/List 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 BaseServiceImplimplements OrganizationService {@Autowiredprivate UserService userService;@Autowiredprivate DictionaryTypeService dictionaryTypeService;@Autowiredprivate CacheUtil cacheUtil;@Autowiredprivate Validator validator;@Overridepublic Organization init() {Organization entity = new Organization();entity.setStatus(StatusEnum.NORMAL.name());return entity;}@Overridepublic void beforeAdd(Organization entity) {// 验证同节点下是否存在同名节点QueryWrapper 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);}}@Overridepublic 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 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);}}@Overridepublic void beforeRemove(Organization entity) {// 验证是否有下级if (super.lambdaQuery().eq(Organization::getParentId, entity.getId()).count() > 0) {throw new CustomException(CommonException.HAS_CHILDREN);}// 验证是否存在人员QueryWrapper queryWrapper = new QueryWrapper<>();queryWrapper.lambda().eq(User::getOrganizationId, entity.getId());long count = userService.count(queryWrapper);if (count > 0) {throw new CustomException(OrganizationExceptionEnum.HAS_USER);}}@Overridepublic void afterAdd(Organization entity) {cacheUtil.set(CacheConstant.ORGANIZATION_CACHE_PREFIX + entity.getId(), entity.getName());}@Overridepublic void afterModify(Organization entity) {cacheUtil.set(CacheConstant.ORGANIZATION_CACHE_PREFIX + entity.getId(), entity.getName());}@Overridepublic 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 queryWrapper = new QueryWrapper<>();queryWrapper.lambda().eq(Organization::getParentId, selectId);List 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;}@Overridepublic void enable(String id) {Organization entity = query(id);entity.setStatus(StatusEnum.NORMAL.name());modify(entity);}@Overridepublic void disable(String id) {Organization entity = query(id);entity.setStatus(StatusEnum.DEAD.name());modify(entity);}@Overridepublic List getParentId(String organizationId) {List 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 {@Autowiredprotected MapperFacade mapperFacade;@Autowiredprotected QueryGenerator queryGenerator;@Autowiredprotected DictionaryUtil dictionaryUtil;@Autowiredprotected 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

欢迎收藏、点赞、评论。

相关内容

热门资讯

秦安莲花干馍:百年传承的非遗美... 🌾 你是否曾经在记忆的长河中,嗅到那股熟悉的麦香?在腊月的秦安县莲花镇,清水河畔的街巷里,传统的味道...
原创 团... 年夜饭的餐桌上,总少不了几道带着记忆温度的老味道。今年我特意整理了六道南北通吃的家常菜,从软糯的年糕...
原创 早... 标题:早餐这样吃,可比油条省事多了,无需揉面无需造型,又营养又好吃。 在忙碌的早晨,我们总是渴望一...
原创 2... 众所周知,中国有南北地区之分,每个地方因为地理环境的差异,在饮食文化方面也有很大的区别,就像大部分南...
告别宴客焦虑:学会这八道压箱底... 招待客人时,总担心饭菜不够丰盛、不够美味,搞得自己焦虑不堪。别愁啦!今天就为你奉上八道压箱底硬菜的详...