项目实战七-项目重构

项目分析

现有项目分析

  1. 项目基本结构:前端->controller->service层->mapper层->数据库
  2. 目前项目情况

     query类(客户端传参)
     	page
     	size
     	data(list): po
     	count
     	pages
        	
     响应(R类封装HashMap):
     	code:
     	msg:
     	data:query
    
  3. 利弊分析
    1. 页面查询用query类,然后将查询结果在赋值到query中
    2. 返回值为R类型,一个HashMap类,通过Rs这个工具类封装query后,返回给客户端
    3. 现状:字段限制条件在po类的字段添加(@BoolNumber、@NotBlank),返回列表数据data,内部存储的item也是po类
    4. 问题:
      1. po应该是对数据库表结构一一对应的类,除了外键修饰,其余都不应该有
      2. 客户端要的数据可能:n张表的组合的结果、一张表的某几个字段、一张表和附加的几个字段
      3. 因此返回给客户端的对象不能用po对象来简单表示,如果对po对象做修改适配,那么将破坏po对象与数据库的一致性

项目整合思路

  1. 客户端传递的参数或者返回给客户端的数据叫Vo(ValueObject)
  2. controller层:控制器拿到Vo对象直接传递给service层/返回给客户端vo数据
    1. 如果想过滤掉一些客户端传递过来的参数(客户端传递的参数有些可能没用),只要有用的,可以将Vo转化为Dto
  3. service层:
    1. 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);
      
      1. 这些T代表的就是po类
    2. 所以传参默认只能是po对象
      1. 增改查:需要将Vo对象转化为po对象
      2. 响应给客户端:需要将po对象转为为Vo对象
    3. 如果想直接传参Vo对象,需要自定义方法,然后传递给mapper层
  4. mapper层:
    1. 如果service层传的的直接是po对象,那么就不需要实现自定义方法
    2. 如果service传递的是Vo对象,就需要通过mapper声明+xml实现的方式实现自定义方法
  5. 总结核心思想
    1. po是直接对应的数据库的表结构字段
    2. Vo直接提供给客户端页面直接使用或者客户端直接传递给后台的数据
      1. 可以自定义一个规则:xxxReqVO都是客户端直接传递的数据,xxxVo是直接返回给客户端的数据
    3. po与Vo之间的转化在service层调用,在mapper层实现
  6. 项目整体结构如下: 图1

改造POJO

  1. 复制JiaKaoBE,改造后的项目为JiaKaoBE2
  2. 之前项目的query,有2个功能:接收客户端传递过来的参数、将返回结果再封装到query中
  3. 改造结果:请求用xxxReqVo,响应用xxxVo
  4. 改造步骤:pojo下新建vo包,在vo下新建以下层级

     ```
     vo
         list  存放xxxVo,专门用来响应给客户端的数据信息
         req         存放客户端传递给controller的数据信息
             page    //存放xxxPageReqVo,专门用来存放客户端传递过来查询列表的数据,即分页查询的Vo
             save    // 存放xxxReqVo,专门用来存放客户端添加、编辑的模型数据信息
     ```
    

改造page

  1. 将原来的query中的类全部移动到vo/req/page下,删除query
    1. PageQuery右击修改类名为PageReqVo,选择所有相关子类,点击确定,则原来query中的所有类都变成了xxxReqVo
    2. KeywordReqVo右击修改类名为KeywordPageReqVo,选择所有相关子类,点击确定,则继承自所有KeywordPageReqVo的子类都变成了xxxPageReqVo
  2. 逐个类修改:只保留客户端请求的参数,删除掉多余返回数据相关的结果,只需要修改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

  1. 之前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);
         }
     }
    
    1. 接收的参数是entity,本质就是po里面的类,按照上面的设计,应该保存的是xxxReqVo对象
  2. 将po文件夹下的所有模型类copy一份到vo/req/save下,并修改所有类的后面添加ReqVo
    1. 删除所有的数据库外键之间的注解、删除一些没有必要传递的属性,只保留需要保持的属性字段
  3. 改造原来po包下面的Model类:删除原来po下所有类的传参限制注解,就是后台校验功能,po只用来一一映射数据库数据,数据库之间关系注解要保留
  4. save下的各个XXXReqVo类代码详情-略

     DictItemReqVo
     DictTypeReqVo
     ExamPlaceCourse
     ExamPlaceReqVoReqVo
     PlateRegionReqVo
    
  5. 改造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);
             }
         }
     }
    
    1. 注意: 为什么要传递2个泛型,因为如果要使用Mybatis-plus的自带方法(增、删、改、查),那么传递给service的,就必须是po类型,即跟数据库保持一致的类型
  6. 所有基于BaseController的子类都要在传递泛型上添加一个类:<Po,PoRequest>,比如:

     public class DictItemController extends BaseController<DictItem, DictItemReqVo> {
         ...
     }
    

改造list

  1. 前面讲过,vo/list下的存放的是返回给客户端的列表数据xxxVo
  2. 注意:这个数据可以是一个po类的某几个字段,也可以是n个po类组合的一个类,比如二级、三级联动的数据
  3. 将save中的类复制到vo/list下:将所有类修改类名,将ReqVo修改为Vo
  4. 将dto下面的类也修改类名,删除Dto修改为Vo,然后拖拽到vo/list下
    1. CityDto修改为CityVo,ProvinceDto修改为ProvinceVo
    2. 对应的ExamPlaceMapper.xml、PlateRegionMapper.xml中的Dto全部修改为Vo
  5. vo/list类如下

     //包含了,二、三级联动的数据
     CityVo
     ProvinceVo
     DictItemVo
     DictTypeVo
     ExamPlaceCourseVo
     ExamPlaceVo
     PlateRegionVo
    

返回值的改造

  1. 请求的返回值常见如下

     {
         code: xxx,
         msg:xxx
     }
     {
         code: xxx,
         msg:xxx,
         data:[]
     }
     {
         code: xxx,
         msg:xxx,
         data:[],
         count:xxx,
     }
    
  2. 可以针对这些常见的返回数据类型,创建相对应的类
  3. 在vo包下新建各种类,专门用来做返回值最基本的结果
    1. 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());
           }
       }
      
    2. 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);
           }
       }
      
    3. 创建一个PageJsonVo类,在DataJsonVo基础上存放一个总数count

       @EqualsAndHashCode(callSuper = true)
       @Data
       public class PageJsonVo<T> extends DataJsonVo<List<T>> {
           private Long count;
       }
      
    4. 创建一个PageVo类

       @Data
       public class PageVo<T> {
           private Long count;
           private Long pages;
           private List<T> data;
       }
          
      
  4. 将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改造

修改控制器的返回值、传参

  1. 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);
             }
         }
     }
    
  2. 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;
         }
     }
    
  3. 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;
         }
     }
    
  4. 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());
         }
     }
    
  5. 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));
         }
     }
    
  6. 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的使用

  1. 问题分析
    1. BaseController中的save方法中,如果想要使用Mybatis-plus的自带方法,需要将ReqVo转化为po类型
    2. DictTypeController中的DataJsonVo<List<DictTypeVo>> list()方法,如果想要使用Mybatis-plus的自带list方法,需要将po类型转化为Vo类型
    3. 那么如何转化呢?
      1. 方法一:通过遍历,一一转化
      2. 方法二:通过引入mapstruct第三方jar包
  2. mapstruct的使用
    1. 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>
      
    2. 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);
       }
      
    3. 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);
       }
      
    4. 在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());
           }
       }
      
    5. 上面代码简化如下

       List<DictTypeVo> vosx = Streams.map(service.list(),MapStructs.INSTANCE::po2vo);
       return JsonVos.ok(vosx);
      
    6. BaseController的save方法改造如下

      1. 在BaseController添加一个抽象方法

         protected abstract Function<ReqVo, Po> getFunction();
        
      2. 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);
             }
         }
        
      3. 所有BaseController的子类都要实现getFunction方法,比如DictItemController

         @Override
         protected Function<DictItemReqVo, DictItem> getFunction() {
             return MapStructs.INSTANCE::reqVo2po;
         }
        

service层改造

  1. 接口改造

     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();
     }
    
  2. 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));
         }
     }
    
  3. impl的所有的list方法改造

    1. 都要添加返回值,PageVo<XXXVo>
    2. 而返回值,将原来的.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);
    }
}
Table of Contents