为什么开发效率低,可能是项目结构有问题!

最近做了一个前后端分离高并发的秒杀书城 ,对项目的代码结构有了新的认识。具体的后台代码实践在这里。对于这个项目,我总结了四点比较重要的项目结构要点,希望对小伙伴们以后的开发中有新的启发。

1. 一定要有返回类型

如今较大型的项目都会用到前后端分离的技术,此时,接口和数据的定义就会显得尤为重要。为了给前端返回统一的用户数据,在一般情况下,我们会为返回值定义一个实体类,其中的属性包括返回码返回描述返回实体,一个例子如下:

{
    "code":200,
    "msg":"ok",
    "data":{
        "this is a response data"
    }
}
  • 我们在代码中可以这样编写

    public class ReturnType {
        /**
         * 状态码
         */
        private int code;
        /**
         * 表明对应请求的返回处理结果 "ok" 或 "fail"
         */
        private String status;
    
        /**
         * 若status=success,则data内返回前端需要的json数据
         * 若status=fail,则data内使用通用的错误码格式
         */
        private Object data;
    
        /**
         * 定义一个通用的创建方法,用于返回值为空的情况
         */
        public static ReturnType create() {
            return ReturnType.create("ok"200);
        }
    
        public static ReturnType create(Object result,int code){
            return ReturnType.create(result,"ok",200);
        }
    
        public static ReturnType create(Object result,String status,int code){
            ReturnType type = new ReturnType();
            type.setStatus(status);
            type.setData(result);
            type.setCode(code);
            return type;
        }
    
        public int getCode() {
            return code;
        }
    
        public int setData(Object code) {
            this.data = code;
        }
        
        public String getStatus() {
            return status;
        }
    
        public void setStatus(String status) {
            this.status = status;
        }
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    }
    

2. 异常抛出的姿势

有过面向对象基础的人都知道,异常是面向对象必不可少的一部分,就Java来说,有ERRORException两大类,由于本片文章主要说的是异常抛出的一些技巧,所以这些基础性的知识可以看这篇文章:Java中的异常

在做Java开发的时候,常常需要我们抛出异常。尤其是前后端分离的项目,在正常情况下,前台给后台发送请求,后台通过处理再把数据返回给前台。但在非正常情况下,譬如库存不够,插入的主键已经存在等异常情况下,我们必须要把这种情况告诉前台,这就用到了我们的自定义异常。

举个例子,如果是库存不够的异常,我应该给前台这样返回:

{
    "code":502,
    "msg":"apple的库存不够",
    "data":{
        "this response shouldn't have response"
    }
}

但是有一个问题,我们如何才能给前台抛出这个优雅的异常呢?

可以用装饰者模式解决这个问题

  • 首先定义一个异常接口,作为抽象组件

    // 装饰者模式中的抽象组件
    public interface ReturnException {
        /**
         * 得到错误代码
         * @return ErrCode
         */
        int getErrCode();
    
        /**
         * 得到错误信息
         * @return ErrMsg
         */
        String getErrMsg();
    
        /**
         * 设置错误信息
         * @param errMsg 错误信息
         */
        void setErrMsg(String errMsg);
    }
    
  • 然后再定义一个异常实现类,作为具体装饰者

    // 具体装饰者
    public class ReturnExceptionImpl extends Exception implements ReturnException {
    
        private ReturnException returnException;
    
        /**
         * 直接接收EmException的传参用于构造业务异常
         * @param returnPtin 错误类型
         */
        public ReturnExceptionImpl(ReturnException returnException) {
            super();
            this.returnException = returnException;
        }
    
        /**
         * 接收自定义errMsg的方式构造业务异常
         * @param returnException 错误类型
         * @param errMsg 错误信息
         */
        public ReturnExceptionImpl(ReturnException returnException,String errMsg){
            this.returnException = returnException;
            this.returnError.setErrMsg(errMsg);
        }
    
        @Override
        public int getErrCode() {
            return this.returnError.getErrCode();
        }
    
        @Override
        public String getErrMsg() {
            return this.returnError.getErrMsg();
        }
    
        @Override
        void ReturnException setErrMsg(String errMsg) {
            this.returnException.setErrMsg(errMsg);
        }
    }
    
  • 还有一个枚举类型,作为装饰者中的具体构件

    // 具体构件
    public enum EmException implements ReturnError {
    
        /**
         * 通用错误类型 999
         */
        PARAMETER_VALIDATION_ERROR(999, "参数不合法"),
        /**
         * 未知错误 888
         */
        UNKNOWN_ERROR(888, "未知异常"),
        /**
         * 10000开头为用户相关信息错误定义
          */
        STOCK_NOT_ENOUGH(502,"库存不够");
    
        private int errCode;
        private String errMsg;
    
        EmException(int errCode, String errMsg) {
            this.errCode = errCode;
            this.errMsg = errMsg;
        }
    
        @Override
        public int getErrCode() {
            return this.errCode;
        }
    
        @Override
        public String getErrMsg() {
            return this.errMsg;
        }
    
        @Override
        public ReturnException setErrMsg(String errMsg) {
            this.errMsg = errMsg;
            return this;
        }
    }
    
  • 之后,我们碰到异常就可以这样抛出

     if (!itemService.decreaseStock(orderDTO.getItemId(),orderDTO.getAmount())){
            throw new ReturnExceptionImpl(EmException.STOCK_NOT_ENOUGH);
      }
    
  • 最后一步,我们在SpringBoot中定义全局异常处理,用来接收全局的异常抛出:

    @ControllerAdvice
    public class GlobalExceptionHandler{
        @ExceptionHandler(Exception.class)
        @ResponseBody
        public ReturnType doError(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Exception ex) {
           	String msg;
            int code;
            if( ex instanceof ReturnException){
                ReturnException businessException = (ReturnException)ex;
                code = EmReturnError.UNKNOWN_ERROR.getErrCode();
                msg = businessException.getErrMsg());
            }else if(ex instanceof ServletRequestBindingException){
                code = EmReturnError.UNKNOWN_ERROR.getErrCode();
                msg = "url绑定路由问题";
            }else if(ex instanceof NoHandlerFoundException){
                code = EmReturnError.UNKNOWN_ERROR.getErrCode();
                msg = "没有找到对应的访问路径";
            }else if (ex instanceof IllegalArgumentException) {
                code = EmReturnError.PARAMETER_VALIDATION_ERROR.getErrCode
                msg = "输入参数不完整";
            }else{
                code = EmReturnError.UNKNOWN_ERROR.getErrCode();
                msg = EmReturnError.UNKNOWN_ERROR.getErrMsg();
            }
            return ReturnType.create(msg,"fail",code);
        }
    }
    

3. 实体类三剑客

所谓的实体类三剑客,即是DO,DTO和VO了。

  • DO,即是DataObject,数据对象。是和数据库的表一对一的,一般情况下由Mybatis的逆向插件直接生成的
  • DTO,即是DataTransferObject,数据转换对象。是对DO层的二次抽象,一般前端的数据会填充到DTO中,即Controller的参数实体一般是DTO
  • VO,即是ViewObject,视图对象。顾名思义,我们后台返回给前台的实体即是VO

对于这三者的关系,我举个例子:大型项目中,用户其他信息和密码在数据库中分开存放的,因为在实际业务中,密码和用户其他信息的读取和修改概率是不一样的。在这种业务场景下,DO就会把用户其他信息和密码存成两个实体类,这是和数据库一一对应的。但是DTO就会把用户其他信息和密码合成一个实体类,因为在业务逻辑中,他们的意义一样重要。当我们把这些数据返回时,前端可能不需要诸如Id一类的数据,我们就会重新把他们封装为VO实体类

以我Github中的秒杀项目为例,如下所示:

在这里插入图片描述

把他们抽象起来来说,就像下图一样:

在这里插入图片描述

此时,又有一个问题诞生了,DO,DTO和VO怎么转化呢?

一般情况下,我们会在它们的所在的层级进行转化:譬如,我们在Service层中把DTO和DO进行相互转化,在Controller层中把DTO和VO进行相互转化。在此我们一定要注意不要手动转化,太过麻烦,我们要善于利用API,Spring给我们提供了BeanUtils.copyProperties()方法,十分方便快捷。如下:

private CategoryDTO convertDtoFromDO(CategoryInfoDO categoryInfoDO, List<ItemCategoryDO> itemCategoryDOList) {
        CategoryDTO categoryDTO = new CategoryDTO();
        BeanUtils.copyProperties(categoryInfoDO,categoryDTO);
        if (itemCategoryDOList != null) {
            categoryDTO.setItemIds(itemCategoryDOList.stream()
                    .map(ItemCategoryDO::getItemId).collect(Collectors.toList()));
        }
        return categoryDTO;
    }

更近一步去思考,如此多的convertxxx,我们有没有可能去简化一下,当然可以!我们可以定义一个反型接口把这些都抽象出来,但是这个就是后话了。

同时,我们还要利用好工具,譬如Lombok。它是通过注解的形式来减轻我们编程的重复劳动。如下

@Data
public class UserDTO() {
    private int id;
    private String name;
    private String pwd;
}

最明显的一个功能就是它可以帮助我们省略实体类中的getter和setter方法,甚至是toString和hashCode,equals等等各种方便的操作。虽然IDEA可以自动生成这些方法,但是IDEA绝对没有它的注解方便省事!

4. 项目包的命名

这个不是绝对的,包括上面三种都不是绝对的。

在我一年多的项目开发过程中,结合我自己和大牛项目结构的包名,我觉得有基本的命名和机构有以下这么几种:

  • config:这个是一些配置,譬如Redis的配置,Druid的配置等等
  • constant:这个会放置常量类,如枚举,常量接口等等
  • controller:这个是控制层。有时候VO实体类所在的vo包也在该包下
  • dao:这个是用来操作数据库的包。mybaits中的mapper也可以存放在这里面
  • entity:这个包存放DO实体类
  • log:存放日志相关操作
  • service:这个是服务层。有时候会把DTO实体类所在的model包也放在该包下
  • util:这个就是防止工具类了,一般情况下,工具类的方法都是静态的,同时该工具类的构造方法也是私有的
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值