- 1.接口规范
- 1.1 路径规范
- 1.2 请求方式规范
- 1.3 API文档描述规范
- 2.入参规范
- 2.1 参数校验:
- 3.异常处理规范
- 3.1 使用SpringMVC的全局异常处理
- 3.2 抛出方式
- 3.3 异常日志记录规范
- 3.3.1 禁止使用e.printStackTrance()(错误的方式)
- 3.3.2 异常堆栈信息被吞(错误的方式)
- 3.3.2 异常信息直接返给前台(错误的方式)
- 3.3.3 正确的方式
- 3.3.4 特殊情况处理
- 4.查询分页规范
- 5.配置规范
- 6.参数校验
- 6.1 使用实体或VO封装校验方法
- 6.2 使用Spring Validation在实体属性上进行校验
- 7.返回值
- 8.工具类规范
@RequestMapping("/api/file/tactics") //以api开头,controller具体功能
public class FileTacticsController {
@PostMapping("/page") //方法具体作用
public ReturnT page(@RequestBody FileTacticsQueryVo vo) {
}
@GetMapping("/get/{id}") //restful风格
public ReturnT getById(@PathVariable(name = "id") Integer id) {
return fileTacticsService.getVoById(id);
}
1.2 请求方式规范
- 传递多个参数使用POST请求,并将多个参数封装到VO中(方便进行校验工作),使用@RequestBody接收json对象。
- 根据id获取某条数据使用GET请求,id参数以resutful风格形式放在请求路径上。如上图。
- 不使用PUT,DELETE等请求方式,因为nginx里开放此类请求方式存在安全问题。
- 使用swagger作为接口文档的管理工具
- 使用注解标明接口所在的模块-子模块,接口所具有的作用
@RestController
@RequestMapping("/api/migrateLog")
@Api(tags = "审计管理-迁移日志") //指明所处模块
public class JobLogController extends BaseController {
@Autowired
private SysConfigService sysConfigService;
@Resource
private JobLogService jobLogService;
//接口所具有的作用,以及请求方式
@ApiOperation(value = "运行日志列表", notes = "运行日志列表", httpMethod = "POST")
@PostMapping("/logPageList")
@OperateLog(module = "审计管理-迁移日志", type = "运行日志列表", des = "运行日志列表")
public ReturnT
2.入参规范
2.1 参数校验:
- 执行条件查询时需要对可空的参数类型进行验证,避免可空的参数穿透到SQL,生成不可预知的SQL查询。
- 尽量避免三个以上的参数(多参数函数),三个以上请封装成对象。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ReturnT exceptionHandler(){
//具体处理逻辑...
}
@ExceptionHandler(BusinessException.class)
public ReturnT businessExceptionHandler(){
//具体处理逻辑...
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ReturnT methodArgumentNotValidExceptionHandler(){
//具体处理逻辑...
}
}
3.2 抛出方式
- 所有异常都向上抛给controller,除特定可预见异常处理除外。
- 创建具体的异常类型,如业务异常、文件异常、网络异常等。
@Override
public void add(JobUserVo vo) {
//...
JobUser existUser = jobUserMapper.selectOne(new QueryWrapper<>(whereUser));
if (existUser != null) {
throw new BusinessException(I18nUtil.getString("user_username_repeat"));
}
//...
}
3.3 异常日志记录规范
3.3.1 禁止使用e.printStackTrance()(错误的方式)
- 此操作会打印堆栈异常,但是未输出到日志文件,上线问题无法排查。
- 如下代码,只记录了异常信息的message,而异常本身并未记录,此操作会导致线上系统出现问题,无法准确定位问题代码和具体异常。
logger.error(e.getMessage());
logger.error("程序发生异常,异常原因{}",e.getMessage())
3.3.2 异常信息直接返给前台(错误的方式)
public ReturnT xxx(){
try{
xxxService.xxx();
}catch(Exception e){
return ReturnT.failed(e.getMessage());
}
}
3.3.3 正确的方式
- 正确的方式应该如下所示,在记录异常message的同时将异常堆栈信息一起记录,即使用Logger接口的void error(String var1, Throwable var2)方法。
try{
}catch(xxxException e){
logger.error(e.getMessage(),e);
}
3.3.4 特殊情况处理
- 在系统中存在许多文件下载功能,前端既要处理文件内容,又要处理出现异常的情况。此时在下载过程中的异常统一抛出FileDownLoadException异常。
@Override
public void downloadFileJobTargetFile(int id, HttpServletResponse response) throws IOException {
JobMigrateInfo jobMigrateInfo = jobMigrateInfoMapper.selectById(id);
if (jobMigrateInfo.getMigrateType() != CommonConstant.FILE_TO_FILE_TACTICS && jobMigrateInfo.getMigrateType() != CommonConstant.DATA_TO_FILE_TACTICS)
throw new FileDownLoadException("任务目的源不是文件,无法提供目标文件下载"); //以此种方式抛出异常,全局异常统一处理,修改Http状态码。
if (jobMigrateInfo.getRunState() != JobMigrateInfo.FINISH_RUN) {
throw new FileDownLoadException("当前任务未完成执行,无法下载迁移目标文件");
}
JobTactics jobTactics = jobTacticsMapper.selectById(jobMigrateInfo.getTacticsId());
FileWriterConfig fileWriterConfig = fileWriterConfigMapper.selectById(jobTactics.getWriteConfigId());
FileTypeEnum fileTypeEnum = FileTypeEnum.convertByTypeCode(fileWriterConfig.getFileType());
File targetFile = new File(fileWriterConfig.getPath() + fileWriterConfig.getFileName() + "." + fileTypeEnum.getExt());
if (!targetFile.exists()) {
throw new FileDownLoadException("下载文件失败,未找到目标文件");
}
ServletUtils.setDownloadFileResponseHeader(response, fileWriterConfig.getFileName() + "." + fileTypeEnum.getExt());
ServletOutputStream outputStream = response.getOutputStream();
FileInputStream fileInputStream = new FileInputStream(targetFile);
IoUtil.copy(fileInputStream, outputStream);
IoUtil.close(fileInputStream);
IoUtil.close(outputStream);
}
4.查询分页规范
- 使用myabtis提供的IPage接口封装分页对象
@Override public IPage5.配置规范getPage(JobUserLogVo log) { IPage page = new Page<>(log.getCurrent(), log.getSize()); QueryWrapper wrapper = new QueryWrapper<>(); return this.baseMapper.selectPage(page, wrapper); } vo @ApiModel("操作日志接收对象") @Data @NoArgsConstructor @AllArgsConstructor public class JobUserLogVo extends JobUserLog { @ApiModelProperty(value = "当前页") private int current; @ApiModelProperty(value = "每页大小") private int size; }
- 将程序中可变的内容抽取到配置文件中进行统一配置管理。
- 创建多个环境的application.yml文件,如application-dev.yml,application-test.yml,application-prod.yml。在application.yml里使用spring.profiles.active指定需要使用的配置环境。
spring:
profiles:
active: dev
- 使用微服务部署时,将配置文件统一配置在配置中心中。
- 此方式写校验方法,由controller层进行调用,自定义校验逻辑简单。使用需要统一校验函数规范化方法名称。
@ApiModel(value = "JobMigrateAddVo")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JobMigrateAddVo {
@ApiModelProperty(value = "id")
private int id;
@ApiModelProperty(value = "任务名")
private String name;
@ApiModelProperty(value = "申请者id")
private int applyId;
@ApiModelProperty(value = "策略id")
private int tacticsId;
@ApiModelProperty(value = "任务执行周期,cron表达式")
private String jobCron;
public boolean formatAndVerify() {
this.jobCron = "* * * * * ? *";
return StringUtils.isNotBlank(name)
&& applyId > 0
&& tacticsId > 0
&& CronExpression.isValidExpression(jobCron);
}
}
6.2 使用Spring Validation在实体属性上进行校验
- 此方式直接使用注解进行校验,快捷方便。针对自定义场景需要自定义校验注解。
@ApiModel("业务应用更新")
public class ReqUpdate {
@ApiModelProperty(value = "业务域ID", required = false)
@JsonProperty
@Pattern(regexp = ValidateUtils.REGEX_NO_HTML_TAG,message = "业务域ID存在非法字符!")
@Size(max = 32,message = "业务域ID长度超过最大32位")
private String businessDomainId;
@ApiModelProperty(value = "业务应用名称", required = false)
@JsonProperty
@Pattern(regexp = ValidateUtils.REGEX_NO_HTML_TAG,message = "业务应用名称存在非法字符!")
@Size(max = 100,message = "业务应用名称长度超过最大100位")
private String name;
@ApiModelProperty(value = "部署级别:1.一级部署 2.二级部署", required = false)
@JsonProperty
@Pattern(regexp = "1|2|",message = "部署级别只能为1或者2")
private String deployLevel;
}
7.返回值
- Controller层使用统一的返回体ReturnT,不能使用Map返回(开发一时爽,维护火葬场)
public class ReturnT8.工具类规范implements Serializable { public static final long serialVersionUID = 42L; //错误码 public static final int SUCCESS_CODE = 200; public static final int FAIL_CODE = 500; public static final int PARAM_ERR_CODE = 555; public static final int REQ_METHOD_ERR_CODE = 556; public static final int UNAUTHORIZED_CODE = 401; public static final int FILE_DOWNLOAD_FAIL_CODE = 586; public static final ReturnT SUCCESS = new ReturnT (SUCCESS_CODE, "执行成功"); public static final ReturnT FAIL = new ReturnT<>(FAIL_CODE, "执行失败"); private int code; private String msg; private T content; public ReturnT() { } public ReturnT(int code, String msg) { this.code = code; this.msg = msg; } public ReturnT(int code, String msg, T content) { this.code = code; this.msg = msg; this.content = content; } public ReturnT(T content) { this.code = SUCCESS_CODE; this.content = content; } public static ReturnT fail(String msg) { return new ReturnT (FAIL_CODE, msg); } public static ReturnT failCode(Integer code, String msg) { return new ReturnT (code, msg); } public static ReturnT failException(Exception e) { if (e instanceof BusinessException) { BusinessException businessException = (BusinessException) e; return ReturnT.failCode(businessException.getCode(), businessException.getMessage()); } else if (e instanceof AccessDeniedException) { return ReturnT.failCode(-1, e.getMessage()); } return new ReturnT (FAIL_CODE, e.getMessage()); } public static ReturnT fail(BusinessErrCode errCode) { return new ReturnT (errCode.getCode(), errCode.getMsg()); } public static ReturnT fail(String msg, T data) { return new ReturnT (FAIL_CODE, msg, data); } public static ReturnT failData(T data) { return new ReturnT (FAIL_CODE, SUCCESS.msg, data); } public static ReturnT succ(String msg) { return new ReturnT (SUCCESS_CODE, msg); } public static ReturnT succData(T data) { return new ReturnT (SUCCESS_CODE, SUCCESS.msg, data); } public static ReturnT succ(String msg, T data) { return new ReturnT (SUCCESS_CODE, msg, data); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getContent() { return content; } public void setContent(T content) { this.content = content; } @Override public String toString() { return "ReturnT [code=" + code + ", msg=" + msg + ", content=" + content + "]"; } }
- 自定义工具类,尽量不要在业务代码里面直接调用第三方的工具类,能更好的与代码逻辑解耦。
- 尽可能的抽象,编写使用范围更大的工具类
举例,假设我们写了一个判断arraylist是否为空的函数,一开始是这样的:
public static boolean isEmpty(ArrayList> list) {
return list == null || list.size() == 0;
}
当有其他类型的集合也需要该方法的时候,就显得很无力。可以改成最抽象的一层去实现,例如:
public static boolean isEmpty(Collection> collection) {
return collection == null || collection.size() == 0;
}
改完后,所有实现了Collection都对象都可以用,适用范围变得更大。
- 使用重载,提供各种类型的入参,调用起来方便,并尽量保证主体代码只有一份,方便维护。



