项目实战三-元数据模块-数据字典条目

项目整合

control层请求参数的封装

  1. 整合点梳理
    1. 控制器中的list方法

       public Map<String,Object> list(long page, long size,String keyword){};
      
    2. 需要接收参数,而且有可能有n个搜索参数,因此可以封装
    3. 前面讲过思路: 将接收参数封装成一个query对象,不同的页面搜索使用不同的query类,而且每次查询的结果封装给这个query对象

       public Map<String,Object> list(DictTypeQuery query){};
      
  2. 在pojo下新增一个query包,下面存放各个页面的query类

     //基类PageQuery
     @Data
     public class PageQuery {
         private static final int DEFAULT_SIZE = 10;
         private long page;
         private long size;
         //当前页的数据
         private List<?> data;
         //总数
         private long count;
         //总页数
         private long pages;
        
         public long getPage() {
             return Math.max(page,1);
         }
        
         public long getSize() {
             return (size < 1) ? DEFAULT_SIZE : size;
         }
     }
     //KeywordQuery
     @EqualsAndHashCode(callSuper = true)
     @Data
     public class KeywordQuery extends PageQuery {
         private String keyword;
     }
     //数据字典类型页面
     @EqualsAndHashCode(callSuper = true)
     @Data
     public class DictTypeQuery extends KeywordQuery {}
     //省份页面
     @EqualsAndHashCode(callSuper = true)
     @Data
     public class ProvinceQuery extends KeywordQuery{}
    
  3. service层更新
    1. 数据字典类型页面

       //DictTypeService
       public interface DictTypeService extends IService<DictType> {
           void list(DictTypeQuery query);
       }
       //DictTypeServiceImpl
       @Service
       @Transactional
       public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {
           @Autowired
           private DictTypeMapper mapper;
           @Override
           public void list(DictTypeQuery query) {
               //配置搜索条件
               LambdaQueryWrapper<DictType> queryWrapper = new LambdaQueryWrapper<>();
               String keyword = query.getKeyword();
               if(!StringUtils.isEmpty(keyword)){
                   queryWrapper.like(DictType::getName,keyword).or()
                           .like(DictType::getValue,keyword).or()
                           .like(DictType::getIntro,keyword);
               }
               //根据id降序
               queryWrapper.orderByDesc(DictType::getId);
              
               MpPage<DictType> page = new MpPage<>(query);
               //查询
               mapper.selectPage(page,queryWrapper);
               //填充结果
               page.updateQuery();
           }
       }
      
    2. 省份页面

       public interface ProvinceService extends IService<Province> {
           void list(ProvinceQuery query);
       }
              
       @Service
       @Transactional
       public class ProvinceServiceImpl extends ServiceImpl<ProvinceMapper, Province> implements ProvinceService {
           @Autowired
           private ProvinceMapper mapper;
           @Override
           public void list(ProvinceQuery query) {
               String keyword = query.getKeyword();
               //配置搜索条件
               LambdaQueryWrapper<Province> queryWrapper = new LambdaQueryWrapper<>();
               if(!StringUtils.isEmpty(keyword)){
                   queryWrapper.like(Province::getName,keyword).or()
                           .like(Province::getPlate,keyword);
               }
               //根据id降序
               queryWrapper.orderByDesc(Province::getId);
               mapper.selectPage(new MpPage<>(query),queryWrapper).updateQuery();
           }
       }
      
  4. MpPage是Page的增强类,用于抽取冗余代码

     //com.zh.jk.common.enhance
     public class MpPage<T> extends Page<T> {
         private  PageQuery query;
         public MpPage(PageQuery query) {
             super(query.getPage(),query.getSize());
             this.query = query;
         }
         public void updateQuery() {
             //填充结果
             query.setCount(getTotal());
             query.setPages(getPages());
             query.setData(getRecords());
             query.setPages(getCurrent());
             query.setSize(getSize());
         }
     }
    
  5. controller
    1. DictTypeController、ProvinceController的list方法如下
     //DictTypeController
     @GetMapping
     public Map<String,Object> list(DictTypeQuery query) {
         service.list(query);
         Map<String,Object> map = new HashMap<>();
         map.put("code",0);
         map.put("msg","");
         map.put("count",query.getCount());
         map.put("data",query.getData());
         return map;
     }
     //ProvinceController
     @GetMapping
     public Map<String,Object> list(ProvinceQuery query) {
         service.list(query);
         Map<String,Object> map = new HashMap<>();
         map.put("code",0);
         map.put("msg","");
         map.put("count",query.getCount());
         map.put("data",query.getData());
         return map;
     }
    

service层优化

  1. 项目的整体思路
    1. controller调用service,serivce调用mapper,mapper调用数据库
    2. service层是依赖于mybatis-plus插件
      1. ClassService继承与IService
      2. ClassServiceImpl继承与ServiceImpl

优化service层(与上面代码进行对比查看)

  1. 优化掉成员mapper
    1. DictTypeServiceImpl继承ServiceImpl<DictTypeMapper, DictType>
    2. 可以看到已经传递了DictTypeMapper泛型,查看ServiceImpl类,内部有个baseMapper成员
    3. 因此DictTypeServiceImpl内部不需要在设置成员,直接使用baseMapper
     public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
     protected Log log = LogFactory.getLog(this.getClass());
        
     //会自动注入M类型的对象
     @Autowired
     protected M baseMapper;
     ...
     }
    
  2. 优化配置搜索条件
    1. 会发现关键字搜索那块,ProvinceServiceImpl/DictTypeServiceImpl二者非常相似,可以抽取
    2. 新建一个MpQueryWrapper类继承自LambdaQueryWrapper,做一个增强,将搜索配置条件那块代码抽取进去
  3. 改造如下
    1. MpQueryWrapper

       //com.zh.jk.common.enhance
       public class MpQueryWrapper<T> extends LambdaQueryWrapper<T> {
           @SafeVarargs
           public final MpQueryWrapper<T> like(Object val, SFunction<T, ?>... funcs) {
               if (val == null) return this;;
               String str = val.toString();
               if (str.length() == 0) return this;
               // this.like(funs[0],val).or().like(funs[1],val).or().like(funs[2],val).or();
               //nested 会自动去掉最后一个or.(),返回值是MpQueryWrapper的父类,因此这要做一个强制转换
               return (MpQueryWrapper<T>) nested((w)->{
                   for (SFunction<T, ?> func : funcs) {
                       w.like(func,str).or();
                   }
               });
              
           }
       }
      
    2. DictTypeServiceImpl

       @Service
       @Transactional
       public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {
           @Override
           public void list(DictTypeQuery query) {
               //配置搜索条件
               MpQueryWrapper<DictType> queryWrapper = new MpQueryWrapper<>();
               queryWrapper.like(query.getKeyword(),DictType::getName,DictType::getValue,DictType::getIntro);
               //根据id降序
               queryWrapper.orderByDesc(DictType::getId);
              
               MpPage<DictType> page = new MpPage<>(query);
               //查询
               //selectPage返回值是MpPage<DictType>
               baseMapper.selectPage(page,queryWrapper);
               //填充结果
               page.updateQuery();
           }
       }
      
    3. ProvinceServiceImpl

       @Service
       @Transactional
       public class ProvinceServiceImpl extends ServiceImpl<ProvinceMapper, Province> implements ProvinceService {
           @Override
           public void list(ProvinceQuery query) {
               String keyword = query.getKeyword();
               //配置搜索条件
               MpQueryWrapper<Province> queryWrapper = new MpQueryWrapper<>();
               queryWrapper.like(query.getKeyword(),Province::getName,Province::getPlate);
               //根据id降序
               queryWrapper.orderByDesc(Province::getId);
               baseMapper.selectPage(new MpPage<>(query),queryWrapper).updateQuery();
           }
       }
      

controller层返回给客户端的结果优化

  1. 通常一个后台项目返回给客户端的n个接口数据格式是一样的,统一的规范,比如规范如下:

     {
         "code": 1,
         "msg": "成功或者失败信息",
         "data":[
             ...
         ]
     }
    
  2. 在pojo下新建一个result包,下面新建一个类R

     //com.zh.jk.pojo.result
     /**
      * 返回给用户的JSON对象
      */
     public class R extends HashMap<String,Object>implements Jsonable  {
         private static final String K_CODE = "code";
         private static final String K_MSG = "msg";
         private static final String K_DATA = "data";
         private static final int CODE_SUCCESS = 0;
         private static final int CODE_ERROR_DEFAULT = CodeMsg.BAD_REQUEST.getCode();
        
         public R() {
             this(true);
         }
         public R(boolean success) {
             this(success,null);
         }
         public R(boolean success, String msg) {
             this(success? CODE_SUCCESS : CODE_ERROR_DEFAULT,msg);
         }
        
         public R(int code,String msg) {
             put(K_CODE,code);
             put(K_MSG,msg);
         }
         public R(CodeMsg codeMsg) {
             this(codeMsg.getCode(),codeMsg.getMsg());
         }
         public R(String msg,Object data) {
             this(data);
             put(K_MSG,msg);
         }
        
         public R(Object data) {
             put(K_CODE,CODE_SUCCESS);
             put(K_DATA,data);
         }
     }
    
  3. 在common下新建一个util包,内部新建一个Rs类,专门针对R类扩展的一些方法

     //com.zh.jk.common.util
     public class Rs {
         private static final String K_COUNT = "count";
         public static R ok(PageQuery query) {
             R r = new R(query.getData());
             r.put(K_COUNT,query.getCount());
             return r;
         }
         public static R error(String msg) {
             return new R(false,msg);
         }
         public static R error(Throwable t) {
             if (t instanceof CommonException){
                 CommonException e = (CommonException)t;
                 return new R(e.getCode(),e.getMessage());
             }else {
                 return error(t.getMessage());
             }
         }
         public static R ok(CodeMsg codeMsg) {
             return new R(true,codeMsg.getMsg());
         }
         public static R ok(String msg) {
             return new R(true,msg);
         }
         public static R ok(Object data) {
             return new R(data);
         }
         public static R ok() {
             return new R();
         }
         public static R raise(String msg) throws CommonException {
             throw new CommonException(msg);
         }
         public static R raise(CodeMsg codeMsg) throws CommonException {
             throw new CommonException(codeMsg);
         }
     }
    
  4. DictTypeController控制器内部方法改造

     @GetMapping
     public R list(DictTypeQuery query) {
         service.list(query);
         return Rs.ok(query);
     }
     @PostMapping("/remove")
     public R remove(String id){
         //id = 10 或者 id = 10,20,30
         String[] idStrs =  id.split(",");
    
         //批量删除
         //数组转化为集合
         //Arrays.asList(idStrs)
         if (service.removeByIds(Arrays.asList(idStrs))){
             return Rs.ok("删除成功");
         }else {
             return Rs.error("删除失败");
         }
     }
     @PostMapping("/save")
     public R save(DictType dictType){
         if ( service.saveOrUpdate(dictType)){
             return Rs.ok("保存成功");
         }else {
             return Rs.error("保存失败");
         }
     }
    

统一异常处理

  1. 如果因为某些原因java代码抛出异常了,通常情况下返回给客户端的是非常长的异常信息,那么可以进行异常的统一处理,友好的返回给客户端
  2. 客户端收到的状态码不能是直接设定,必须是异常形式抛出,才会有效
  3. result包下新建一个CodeMsg枚举类,存放各种结果编码

     /常用编码
     public enum CodeMsg {
         BAD_REQUEST(400, "请求出错"),
         UNAUTHORIZED(401, "未授权"),
         FORBIDDEN(403, "禁止访问"),
         NOT_FOUND(404, "资源不存在"),
         INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
        
         OPERATE_OK(0, "操作成功"),
         SAVE_OK(0, "保存成功"),
         REMOVE_OK(0, "删除成功"),
        
         OPERATE_ERROR(40001, "操作失败"),
         SAVE_ERROR(40002, "保存失败"),
         REMOVE_ERROR(40003, "删除失败"),
         UPLOAD_IMG_ERROR(40004, "图片上传失败"),
        
         WRONG_USERNAME(50001, "用户名不存在"),
         WRONG_PASSWORD(50002, "密码错误"),
         USER_LOCKED(50003, "用户被锁定,无法正常登录"),
         WRONG_CAPTCHA(50004, "验证码错误"),
        
         NO_TOKEN(60001, "没有Token,请登录"),
         TOKEN_EXPIRED(60002, "Token过期,请重新登录"),
         NO_PERMISSION(60003, "没有相关的操作权限");
        
         private final int code;
         private final String msg;
        
         CodeMsg(int code, String msg) {
             this.code = code;
             this.msg = msg;
         }
        
         public int getCode() {
             return code;
         }
        
         public String getMsg() {
             return msg;
         }
     }
    
  4. enhance下新建一个Jsonable,只要一个类实现这个接口就可以将对象转化为json字符串,让R类实现

     public interface Jsonable {
         default String jsonString() throws Exception {
             ObjectMapper mapper = new ObjectMapper();
             //如果json对象的属性为null,则不需要转化
             mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
             return mapper.writeValueAsString(this);
         }
     }
    
    1. 需要在application.yml中添加配置

       spring:
         # 如果json中的某个属性值为null,这不需要转化为json字符串
         jackson:
           default-property-inclusion: non_null
      
  5. common包下新建一个exception包,自定义异常类CommonException

     //com.zh.jk.common.exception
     @EqualsAndHashCode(callSuper = true)
     @Data
     public class CommonException extends RuntimeException {
         private int code;
        
         public CommonException() {
             this(CodeMsg.BAD_REQUEST.getCode(), null);
         }
        
         public CommonException(String msg) {
             this(msg, null);
         }
        
         public CommonException(int code, String msg) {
             this(code, msg, null);
         }
        
         public CommonException(String msg, Throwable cause) {
             this(CodeMsg.BAD_REQUEST.getCode(), msg, cause);
         }
        
         public CommonException(int code, String msg, Throwable cause) {
             super(msg, cause);
             this.code = code;
         }
        
         public CommonException(CodeMsg codeMsg) {
             this(codeMsg, null);
         }
        
         public CommonException(CodeMsg codeMsg, Throwable cause) {
             this(codeMsg.getCode(), codeMsg.getMsg(), cause);
         }
        
         public int getCode() {
             return code;
         }
     }
    

springMVC处理异常

  1. springMVC可以通过给一个类添加(@ControllerAdvice)注解,拦截所有异常
    1. @ControllerAdvice(basePackages = "com.zh.jk"): basePackages表示扫描所需要异常处理的包
  2. 定义相关异常的方法,方法上加入注解@ExceptionHandler(value属性为异常的类)
    1. 一旦抛出异常会主动调用该方法
    2. 具体异常:@ExceptionHandler(value = MethodArgumentNotValidException.class)
    3. 未知异常:@ExceptionHandler(value = Throwable.class)
  3. 定义CommonExceptionHandler类,统一拦截所有异常

     //com.zh.jk.common.exception
     //拦截所有异常
     @ControllerAdvice
     public class CommonExceptionHandler {
         @ExceptionHandler(Throwable.class)
         public void handle(Throwable t, HttpServletRequest request, HttpServletResponse response) throws Exception {
             //设置返回数据的格式
             response.setContentType("application/json;charset=UTF-8");
             //所有异常都为400
             response.setStatus(400);
             //异常信息写回给客户端
             response.getWriter().write(Rs.error(t).jsonString());
             //在debug下才会打印
             Debugs.run(t::printStackTrace);
         }
     }
    
    1. 在util包下新建Debugs,专门用于设置整个项目是否是debug环境

       package com.zh.jk.common.util;
       public class Debugs {
           public static final boolean DEBUG = false;
              
           public static void run(Runnable runnable) {
               if (!DEBUG) return;
               if (runnable == null) return;
               runnable.run();
           }
       }
      
  4. 使用@RestControllerAdvice简化CommonExceptionHandler
    1. 该注解是@ControllerAdvice、@ResponseBody的组合注解

       //拦截所有异常
       @RestControllerAdvice
       @Slf4j
       public class CommonExceptionHandler {
           @ExceptionHandler(Throwable.class)
           @ResponseStatus(code = HttpStatus.BAD_REQUEST)
           public R handle(Throwable t)  {
               log.error(null,t);
               return Rs.error(t);
           }
       }
      
    2. Slf4j代替Debugs
    3. ResponseStatus代替response.setStatus
    4. 直接返回R可以直接转json
  5. DictTypeController控制器内部方法改造

     @PostMapping("/remove")
     public R remove(String id){
         //id = 10 或者 id = 10,20,30
         String[] idStrs =  id.split(",");
         //批量删除
         //数组转化为集合
         //Arrays.asList(idStrs)
         if (service.removeByIds(Arrays.asList(idStrs))){
             return Rs.ok("删除成功");
         }else {
             throw new CommonException("删除失败");
         }
     }
     @PostMapping("/save")
     public R save(DictType dictType){
         if ( service.saveOrUpdate(dictType)){
             return Rs.ok("保存成功");
         }else {
             throw new CommonException("保存失败");
         }
     }
    

数据字典条目开发

  1. 新建查询条件类DictItemQuery

     //com.zh.jk.pojo.query
     @EqualsAndHashCode(callSuper = true)
     @Data
     public class DictItemQuery extends KeywordQuery {
         /**
          * 所属的类型
          */
         private Integer typeId;
     }
    
  2. service层

     //com.zh.jk.service
     public interface DictItemService extends IService<DictItem> {
         void list(DictItemQuery query);
     }
     //DictItemServiceImpl
     @Service
     @Transactional
     public class DictItemServiceImpl extends ServiceImpl<DictItemMapper, DictItem> implements DictItemService {
        
         @Override
         public void list(DictItemQuery query) {
             //查询条件
             MpQueryWrapper<DictItem> wrapper = new MpQueryWrapper<>();
             wrapper.like(query.getKeyword(),DictItem::getName,DictItem::getValue);
             Integer typeId = query.getTypeId();
             //联合外键查询!!!上面用or,这个要用eq,相当于AND
             if (typeId != null && typeId >0 ){
                 wrapper.eq(DictItem::getTypeId,typeId);
             }
             //排序方式
             wrapper.orderByDesc(DictItem::getId);
             //查询
             baseMapper.selectPage(new MpPage<>(query),wrapper).updateQuery();
         }
     }
    
  3. controller层改造

    1. 新建一个BaseController,抽取基层代码

       public abstract class BaseController<T> {
      
           protected abstract IService<T> getService();
              
           @PostMapping("/remove")
           public R remove(String id){
               //id = 10 或者 id = 10,20,30
               String[] idStrs =  id.split(",");
              
               //批量删除
               //数组转化为集合
               //Arrays.asList(idStrs)
               if (getService().removeByIds(Arrays.asList(idStrs))){
                   //return Rs.ok("删除成功");
                   return Rs.ok(CodeMsg.REMOVE_OK);
               }else {
                   //throw new CommonException("删除失败");
                   return Rs.raise(CodeMsg.REMOVE_ERROR);
                          
               }
           }
           @PostMapping("/save")
           public R save(T entity){
               if ( getService().saveOrUpdate(entity)){
                   //return Rs.ok("保存成功");
                   return Rs.ok(CodeMsg.SAVE_OK);
               }else {
                   //throw new CommonException("保存失败");
                   return Rs.raise(CodeMsg.SAVE_ERROR);
               }
           }
       }
      
    2. DictItemController/DictTypeController/ProvinceController如下:

       //DictItemController
       @RestController
       @RequestMapping("/dictItems")
       public class DictItemController extends BaseController<DictItem> {
           @Autowired
           private DictItemService service;
           @GetMapping
           public R list(DictItemQuery query) {
               service.list(query);
               return Rs.ok(query);
           }
           @Override
           protected IService<DictItem> getService() {
               return service;
           }
       }
       //DictTypeController
       @RestController
       @RequestMapping("/dictTypes")
       public class DictTypeController extends BaseController<DictType> {
           @Autowired
           private DictTypeService service;
           @GetMapping
           public R list(DictTypeQuery query) {
               service.list(query);
               return Rs.ok(query);
           }
           //获取所有数据,不分页查询
           @GetMapping("/list")
           public R list() {
               return Rs.ok(service.list());
           }
           @Override
           protected IService<DictType> getService() {
               return service;
           }
       }
       //ProvinceController
       @RestController
       @RequestMapping("/provinces")
       public class ProvinceController extends BaseController<Province> {
           @Autowired
           private ProvinceService service;
           @GetMapping
           public Map<String,Object> list(ProvinceQuery query) {
               service.list(query);
               return Rs.ok(query);
           }
           @Override
           protected IService<Province> getService() {
               return service;
           }
       }
      
    3. 前端代码改造

      1. metadata下新建dictItem文件夹,下面新建list.html/save.html,代码略
Table of Contents