项目实战七-项目重构
项目分析
现有项目分析
- 项目基本结构:前端->controller->service层->mapper层->数据库
-
目前项目情况
query类(客户端传参) page size data(list): po count pages 响应(R类封装HashMap): code: msg: data:query
- 利弊分析
- 页面查询用query类,然后将查询结果在赋值到query中
- 返回值为R类型,一个HashMap类,通过Rs这个工具类封装query后,返回给客户端
- 现状:字段限制条件在po类的字段添加(@BoolNumber、@NotBlank),返回列表数据data,内部存储的item也是po类
- 问题:
- po应该是对数据库表结构一一对应的类,除了外键修饰,其余都不应该有
- 客户端要的数据可能:n张表的组合的结果、一张表的某几个字段、一张表和附加的几个字段
- 因此返回给客户端的对象不能用po对象来简单表示,如果对po对象做修改适配,那么将破坏po对象与数据库的一致性
项目整合思路
- 客户端传递的参数或者返回给客户端的数据叫Vo(ValueObject)
- controller层:控制器拿到Vo对象直接传递给service层/返回给客户端vo数据
- 如果想过滤掉一些客户端传递过来的参数(客户端传递的参数有些可能没用),只要有用的,可以将Vo转化为Dto
- service层:
-
service使用的是Mybatis-Plus,都是默认的方法,BaseMapper中默认的相关方法如下:
boolean saveOrUpdate(T entity); int insert(T entity); int updateById(@Param("et") T entity); int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper); List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
- 这些T代表的就是po类
- 所以传参默认只能是po对象
- 增改查:需要将Vo对象转化为po对象
- 响应给客户端:需要将po对象转为为Vo对象
- 如果想直接传参Vo对象,需要自定义方法,然后传递给mapper层
-
- mapper层:
- 如果service层传的的直接是po对象,那么就不需要实现自定义方法
- 如果service传递的是Vo对象,就需要通过mapper声明+xml实现的方式实现自定义方法
- 总结核心思想
- po是直接对应的数据库的表结构字段
- Vo直接提供给客户端页面直接使用或者客户端直接传递给后台的数据
- 可以自定义一个规则:xxxReqVO都是客户端直接传递的数据,xxxVo是直接返回给客户端的数据
- po与Vo之间的转化在service层调用,在mapper层实现
- 项目整体结构如下:
改造POJO
- 复制JiaKaoBE,改造后的项目为JiaKaoBE2
- 之前项目的query,有2个功能:接收客户端传递过来的参数、将返回结果再封装到query中
- 改造结果:请求用xxxReqVo,响应用xxxVo
-
改造步骤:pojo下新建vo包,在vo下新建以下层级
``` vo list 存放xxxVo,专门用来响应给客户端的数据信息 req 存放客户端传递给controller的数据信息 page //存放xxxPageReqVo,专门用来存放客户端传递过来查询列表的数据,即分页查询的Vo save // 存放xxxReqVo,专门用来存放客户端添加、编辑的模型数据信息 ```
改造page
- 将原来的query中的类全部移动到vo/req/page下,删除query
- PageQuery右击修改类名为PageReqVo,选择所有相关子类,点击确定,则原来query中的所有类都变成了xxxReqVo
- KeywordReqVo右击修改类名为KeywordPageReqVo,选择所有相关子类,点击确定,则继承自所有KeywordPageReqVo的子类都变成了xxxPageReqVo
-
逐个类修改:只保留客户端请求的参数,删除掉多余返回数据相关的结果,只需要修改PageReqVo
@Data public class PageReqVo { private static final int DEFAULT_SIZE = 10; private long page; private long size; public long getPage() { return Math.max(page,1); } public long getSize() { return (size < 1) ? DEFAULT_SIZE : size; } }
改造save
-
之前BaseController统一封装了保存功能
@PostMapping("/save") public R save(@Valid T entity){ if (getService().saveOrUpdate(entity)){ return Rs.ok(CodeMsg.SAVE_OK); }else { return Rs.raise(CodeMsg.SAVE_ERROR); } }
- 接收的参数是entity,本质就是po里面的类,按照上面的设计,应该保存的是xxxReqVo对象
- 将po文件夹下的所有模型类copy一份到vo/req/save下,并修改所有类的后面添加ReqVo
- 删除所有的数据库外键之间的注解、删除一些没有必要传递的属性,只保留需要保持的属性字段
- 改造原来po包下面的Model类:删除原来po下所有类的传参限制注解,就是后台校验功能,po只用来一一映射数据库数据,数据库之间关系注解要保留
-
save下的各个XXXReqVo类代码详情-略
DictItemReqVo DictTypeReqVo ExamPlaceCourse ExamPlaceReqVoReqVo PlateRegionReqVo
-
改造BaseController
@Validated public abstract class BaseController<Po,ReqVo> { protected abstract IService<Po> getService(); //remove方法,不变,略 @PostMapping("/save") public R save(@Valid ReqVo reqVo){ //如果要用Mybatis-plus的自带方法,那么传参对象必须是po类型,因此这里需要将reqVo转化为Po Po po = null; //saveOrUpdate是Mybatis-plus内部封装方法 if (getService().saveOrUpdate(po)){ return Rs.ok(CodeMsg.SAVE_OK); }else { return Rs.raise(CodeMsg.SAVE_ERROR); } } }
- 注意: 为什么要传递2个泛型,因为如果要使用Mybatis-plus的自带方法(增、删、改、查),那么传递给service的,就必须是po类型,即跟数据库保持一致的类型
-
所有基于BaseController的子类都要在传递泛型上添加一个类:<Po,PoRequest>,比如:
public class DictItemController extends BaseController<DictItem, DictItemReqVo> { ... }
改造list
- 前面讲过,vo/list下的存放的是返回给客户端的列表数据xxxVo
- 注意:这个数据可以是一个po类的某几个字段,也可以是n个po类组合的一个类,比如二级、三级联动的数据
- 将save中的类复制到vo/list下:将所有类修改类名,将ReqVo修改为Vo
- 将dto下面的类也修改类名,删除Dto修改为Vo,然后拖拽到vo/list下
- CityDto修改为CityVo,ProvinceDto修改为ProvinceVo
- 对应的ExamPlaceMapper.xml、PlateRegionMapper.xml中的Dto全部修改为Vo
-
vo/list类如下
//包含了,二、三级联动的数据 CityVo ProvinceVo DictItemVo DictTypeVo ExamPlaceCourseVo ExamPlaceVo PlateRegionVo
返回值的改造
-
请求的返回值常见如下
{ code: xxx, msg:xxx } { code: xxx, msg:xxx, data:[] } { code: xxx, msg:xxx, data:[], count:xxx, }
- 可以针对这些常见的返回数据类型,创建相对应的类
- 在vo包下新建各种类,专门用来做返回值最基本的结果
-
JsonVo
//com.zh.jk.pojo.vo @EqualsAndHashCode(callSuper = true) @Data public class JsonVo { private static final int CODE_OK = CodeMsg.OPERATE_OK.getCode(); private static final int CODE_ERROR = CodeMsg.BAD_REQUEST.getCode(); private Integer code; private String msg; public JsonVo() { this(true); } public JsonVo(boolean ok) { this(ok, null); } public JsonVo(boolean ok, String msg) { this(ok ? CODE_OK : CODE_ERROR, msg); } public JsonVo(int code, String msg) { this.code = code; this.msg = msg; } public JsonVo(CodeMsg codeMsg) { this(codeMsg.getCode(), codeMsg.getMsg()); } }
-
DataJsonVo
@EqualsAndHashCode(callSuper = true) @Data public class DataJsonVo<T> extends JsonVo{ private T data; public DataJsonVo() {} public DataJsonVo(String msg, T data) { super(true, msg); this.data = data; } public DataJsonVo(T data) { this(null, data); } }
-
创建一个PageJsonVo类,在DataJsonVo基础上存放一个总数count
@EqualsAndHashCode(callSuper = true) @Data public class PageJsonVo<T> extends DataJsonVo<List<T>> { private Long count; }
-
创建一个PageVo类
@Data public class PageVo<T> { private Long count; private Long pages; private List<T> data; }
-
-
将common/util/Rs修改为JsonVos,专门用于封装返回数据类型的工具类(注意:之前返回客户端的是R类即hashmap类,现在返回的是对象)
public class JsonVos { public static JsonVo error(String msg) { return new JsonVo(false, msg); } public static JsonVo error(CodeMsg msg) { return new JsonVo(msg); } public static JsonVo error(int code, String msg) { return new JsonVo(code, msg); } public static JsonVo error() { return new JsonVo(false); } public static JsonVo error(Throwable t) { if (t instanceof CommonException) { CommonException ce = (CommonException) t; return new JsonVo(ce.getCode(), ce.getMessage()); } else if (t instanceof BindException) { BindException be = (BindException) t; List<ObjectError> errors = be.getBindingResult().getAllErrors(); // 函数式编程的方式:stream List<String> defaultMsgs = errors .stream().map(ObjectError::getDefaultMessage) .collect(Collectors.toList()); // List<String> defaultMsgs = new ArrayList<>(errors.size()); // for (ObjectError error : errors) { // defaultMsgs.add(error.getDefaultMessage()); // } String msg = StringUtils.collectionToDelimitedString(defaultMsgs, ", "); return error(msg); } else if (t instanceof ConstraintViolationException) { ConstraintViolationException cve = (ConstraintViolationException) t; List<String> msgs = cve.getConstraintViolations() .stream().map(ConstraintViolation::getMessage) .collect(Collectors.toList()); String msg = StringUtils.collectionToDelimitedString(msgs, ", "); return error(msg); } else { // 其他异常 // return error(t.getMessage()); return error(); } } public static JsonVo ok(CodeMsg msg) { return new JsonVo(msg); } public static <T> PageJsonVo<T> ok(PageVo<T> pageVo) { PageJsonVo<T> pageJsonVo = new PageJsonVo<>(); pageJsonVo.setCount(pageVo.getCount()); pageJsonVo.setData(pageVo.getData()); return pageJsonVo; } public static JsonVo ok(String msg) { return new JsonVo(true, msg); } public static <T> DataJsonVo<T> ok(T data) { return new DataJsonVo<>(data); } public static JsonVo ok() { return new JsonVo(); } public static <T> T raise(String msg) throws CommonException { throw new CommonException(msg); } public static <T> T raise(CodeMsg codeMsg) throws CommonException { throw new CommonException(codeMsg); } }
controller改造
修改控制器的返回值、传参
-
BaseController
@Validated public abstract class BaseController<Po,ReqVo> { protected abstract IService<Po> getService(); @PostMapping("/remove") public JsonVo remove(@NotBlank(message = "id不能为空") String id){ String[] idStrs = id.split(","); //批量删除 //数组转化为集合 if (getService().removeByIds(Arrays.asList(idStrs))){ return JsonVos.ok(CodeMsg.REMOVE_OK); }else { return JsonVos.raise(CodeMsg.REMOVE_ERROR); } } @PostMapping("/save") public JsonVo save(@Valid ReqVo reqVo){ //如果要用Mybatis-plus的自带方法,那么传参对象必须是po类型,因此这里需要将reqVo转化为Po Po po = null; if ( getService().saveOrUpdate(po)){ return JsonVos.ok(CodeMsg.SAVE_OK); }else { return JsonVos.raise(CodeMsg.SAVE_ERROR); } } }
-
DictItemController
@RestController @RequestMapping("/dictItems") public class DictItemController extends BaseController<DictItem, DictItemReqVo> { @Autowired private DictItemService service; @GetMapping public PageJsonVo<DictItemVo> list(DictItemPageReqVo query) { return JsonVos.ok(service.list(query)); } @Override protected IService<DictItem> getService() { return service; } }
-
DictTypeController
@RestController @RequestMapping("/dictTypes") public class DictTypeController extends BaseController<DictType, DictTypeReqVo> { @Autowired private DictTypeService service; @GetMapping public PageJsonVo<DictTypeVo> list(DictTypePageReqVo query) { return JsonVos.ok(service.list(query)); } //获取所有数据,不分页查询 @GetMapping("/list") public DataJsonVo<List<DictTypeVo>> list() { //service.list()调用的是Mybatis-plus的内置方法,那么返回的结果是List<DictType>类型 //如何转化成List<DictTypeVo>呢?使用mapstruct第三方jar包 return JsonVos.ok(service.list()); } @Override protected IService<DictType> getService() { return service; } }
-
ExamPlaceController
@RestController @RequestMapping("/examPlaces") public class ExamPlaceController extends BaseController<ExamPlace, ExamPlaceReqVo> { @Autowired private ExamPlaceService service; @Override protected IService<ExamPlace> getService() { return service; } @GetMapping public PageJsonVo<ExamPlaceVo> list(ExamPlacePageReqVo query) { return JsonVos.ok(service.list(query)); } @GetMapping("/regionExamPlaces") public DataJsonVo<List<ProvinceVo>> listRegionExamPlaces() { return JsonVos.ok(service.listRegionExamPlaces()); } }
-
ExamPlaceCourseController
@RestController @RequestMapping("/examPlaceCourses") public class ExamPlaceCourseController extends BaseController<ExamPlaceCourse, ExamPlaceCourseReqVo> { @Autowired private ExamPlaceCourseService service; @Override protected IService<ExamPlaceCourse> getService() { return service; } @GetMapping public PageJsonVo<ExamPlaceCourseVo> list(ExamPlaceCoursePageReqVo query) { return JsonVos.ok(service.list(query)); } }
-
PlateRegionController
@RestController @RequestMapping("/plateRegions") public class PlateRegionController extends BaseController<PlateRegion, PlateRegionReqVo>{ @Autowired private PlateRegionService service; @Override protected IService<PlateRegion> getService() { return service; } @GetMapping("/provinces") public PageJsonVo<PlateRegionVo> listProvinces(ProvincePageReqVo query){ return JsonVos.ok(service.listProvinces(query)); } //查询所有省份不分页 @GetMapping("/provinces/list") public DataJsonVo<List<PlateRegionVo>> listProvinces(){ return JsonVos.ok(service.listProvinces()); } @GetMapping("/cities") public PageJsonVo<PlateRegionVo> listCities(CityPageReqVo query){ return JsonVos.ok(service.listCities(query)); } /* * 返回所有的省份+城市,树状结构 * */ @GetMapping("/regions") public DataJsonVo<List<ProvinceVo>> listRegions(){ return JsonVos.ok(service.listRegions()); } }
mapstruct的使用
- 问题分析
- BaseController中的save方法中,如果想要使用Mybatis-plus的自带方法,需要将ReqVo转化为po类型
- DictTypeController中的
DataJsonVo<List<DictTypeVo>> list()
方法,如果想要使用Mybatis-plus的自带list方法,需要将po类型转化为Vo类型 - 那么如何转化呢?
- 方法一:通过遍历,一一转化
- 方法二:通过引入mapstruct第三方jar包
- mapstruct的使用
-
pom.xml添加依赖
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.4.1.Final</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.1.Final</version> <scope>provided</scope> </dependency>
-
common包下新建mapStruct包,下面新建MapStructs接口
/** * ReqVo -> Po 将客户端传递的ReqVo转化为Vo对象 * Po -> Vo 通过Mybits-plus到数据库查询的po对象转为Vo对象传递给客户端 */ @Mapper public interface MapStructs { MapStructs INSTANCE = Mappers.getMapper(MapStructs.class); /**po-vo***/ DictItemVo po2vo(DictItem po); DictTypeVo po2vo(DictType po); ExamPlaceVo po2vo(ExamPlace po); PlateRegionVo po2vo(PlateRegion po); ExamPlaceCourseVo po2vo(ExamPlaceCourse po); /**vo-po***/ DictItem reqVo2po(DictItemReqVo reqVo); DictType reqVo2po(DictTypeReqVo reqVo); ExamPlace reqVo2po(ExamPlaceReqVo reqVo); PlateRegion reqVo2po(PlateRegionReqVo reqVo); ExamPlaceCourse reqVo2po(ExamPlaceCourseReqVo reqVo); }
-
DictTypeController中的
DataJsonVo<List<DictTypeVo>> list()
方法://获取所有数据,不分页查询 @GetMapping("/list") public DataJsonVo<List<DictTypeVo>> list() { //service.list()调用的是Mybatis-plus的内置方法,那么返回的结果是List<DictType>类型 //如何转化成List<DictTypeVo>呢?使用mapstruct第三方jar包 List<DictTypeVo> vos = service.list().stream().map(dictType -> { return MapStructs.INSTANCE.po2vo(dictType); }).collect(Collectors.toList()); return JsonVos.ok(vos); }
-
在common/util/下封装一个Streams类
public class Streams { public static <T, R> List<R> map(Collection<T> list, Function<T, R> function) { return list.stream().map(function).collect(Collectors.toList()); } }
-
上面代码简化如下
List<DictTypeVo> vosx = Streams.map(service.list(),MapStructs.INSTANCE::po2vo); return JsonVos.ok(vosx);
-
BaseController的save方法改造如下
-
在BaseController添加一个抽象方法
protected abstract Function<ReqVo, Po> getFunction();
-
save方法改造如下
@PostMapping("/save") public JsonVo save(@Valid ReqVo reqVo){ //如果要用Mybatis-plus的自带方法,那么传参对象必须是po类型,因此这里需要将reqVo转化为Po Po po = getFunction().apply(reqVo); if ( getService().saveOrUpdate(po)){ return JsonVos.ok(CodeMsg.SAVE_OK); }else { return JsonVos.raise(CodeMsg.SAVE_ERROR); } }
-
所有BaseController的子类都要实现getFunction方法,比如DictItemController
@Override protected Function<DictItemReqVo, DictItem> getFunction() { return MapStructs.INSTANCE::reqVo2po; }
-
-
service层改造
-
接口改造
public interface DictItemService extends IService<DictItem> { PageVo<DictItemVo> list(DictItemPageReqVo query); } public interface DictTypeService extends IService<DictType> { PageVo<DictTypeVo> list(DictTypePageReqVo query); } public interface ExamPlaceCourseService extends IService<ExamPlaceCourse> { PageVo<ExamPlaceCourseVo> list(ExamPlaceCoursePageReqVo query); } public interface ExamPlaceService extends IService<ExamPlace> { PageVo<ExamPlaceVo> list(ExamPlacePageReqVo query); List<ProvinceVo> listRegionExamPlaces(); } public interface PlateRegionService extends IService<PlateRegion> { PageVo<PlateRegionVo> listProvinces(ProvincePageReqVo query); PageVo<PlateRegionVo> listCities(CityPageReqVo query); List<PlateRegionVo> listProvinces(); List<ProvinceVo> listRegions(); }
-
MpPage改造如下
public class MpPage<T> extends Page<T> { private final PageReqVo reqVo; public MpPage(PageReqVo query) { super(query.getPage(),query.getSize()); this.reqVo = query; } private <N> PageVo<N> commonBuldVo(List<N> data) { reqVo.setPage(getCurrent()); reqVo.setSize(getSize()); PageVo<N> pageVo = new PageVo<>(); pageVo.setCount(getTotal()); pageVo.setPages(getPages()); pageVo.setData(data); return pageVo; } public PageVo<T> buildVo() { return commonBuldVo(getRecords()); } public <R> PageVo<R> buildVo(Function<T, R> function) { return commonBuldVo(Streams.map(getRecords(), function)); } }
-
impl的所有的list方法改造
- 都要添加返回值,
PageVo<XXXVo>
- 而返回值,将原来的
.updateQuery()
改成buildVo(MapStructs.INSTANCE::po2vo)
即可
- 都要添加返回值,
CommonExceptionHandler改造
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {
@ExceptionHandler(Throwable.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public JsonVo handle(Throwable t) {
log.error(null,t);
return JsonVos.error(t);
}
}