最近做了一个前后端分离高并发的秒杀书城 ,对项目的代码结构有了新的认识。具体的后台代码实践在这里。对于这个项目,我总结了四点比较重要的项目结构要点,希望对小伙伴们以后的开发中有新的启发。
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来说,有ERROR
和Exception
两大类,由于本片文章主要说的是异常抛出的一些技巧,所以这些基础性的知识可以看这篇文章: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:这个就是防止工具类了,一般情况下,工具类的方法都是静态的,同时该工具类的构造方法也是私有的