Validation校验框架
传统代码开发,接口参数基本校验一般在Controller层完成,在校验时会写很多的if判断,导致代码冗长。幸运的是lombok的Validation校验框架提供了很多注解,运用这些注解做参数校验就变得非常简单了。具体做法是在Controller层方法入口参数上加上@Valid注解开启参数验证
@PostMapping("/save")
@ApiOperation("生成提货任务")
public Result save(@RequestBody @Valid TakeOutDto takeOutDto) {
log.info("生成提货任务,参数:{}", takeOutDto);
return Results.newSuccessResult();
}
在实体属性上加上对应注解
@Data
public class TakeOutDto {
// 提货方式
@NotNull(message = "请选择提货方式")
private Integer takeOutCheck;
// 出库负责人
private Integer operator;
// 提货要求
@Size(max = 500, message = "提货要求太长了")
private String comments;
}
这样就完成了对属性takeOutCheck和comments的校验,是不是很方便。
统一异常处理
代码开发业务处理中会遇到各种情况,抛出各种异常,甚至会有我们没有处理到的异常抛到页面上,让用户看到一堆无法理解的异常信息,即不友好也不安全,所以找一个方法把异常统一处理掉,以我们规定的形式反馈出去就变得非常有必要了。
首先在项目中,我们可能定义了一堆异常类,用于在不同场景下的异常处理
异常基类
/**
* 异常基类
* @author: sts
* @since: 2021/5/19 15:59
*/
public class BaseException extends RuntimeException {
int code;
protected BaseException(String message, int code) {
super(message);
this.code = code;
}
}
具体异常类
/**
* 参数异常类
* @author: sts
* @since: 2021/6/24 16:20
*/
public class ParamException extends BaseException {
public ParamException(String message) {
super(message, CommonStateCode.PARAM_ERROR);
}
}
在业务处理中使用这些异常类抛出异常
@Override
@Transactional(rollbackFor = Exception.class)
public void discard(TakeOutVo takeOutVo) {
TakeOutEntity takeOutEntity = takeOutRepo.getById(takeOutVo.getId());
if (1 == takeOutEntity.getIsDeleted()) {
throw new ParamException("任务已被删除");
}
}
此时代码运行到异常抛出行就会把异常信息抛到页面,显示我们提供的异常描述信息。页面要想友好的提醒用户,就要处理这个异常,需要判断接口返回是否正常,那有没有一种办法让接口返回统一的格式方便接收者处理呢,答案是肯定的,这就是统一异常处理。
利用@ExceptionHandler注解,分别处理不同的异常
/**
* 全局异常处理器
*
* @author Alan
* @since 2021-12-30 14:08
**/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义异常
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result handleBusinessException(BusinessException e) {
log.error(e.getMessage(), e);
return Results.newFailedResult(CommonStateCode.BUSI_ERROR, e.getMessage());
}
/**
* 处理自定义异常
*/
@ExceptionHandler(ParamException.class)
@ResponseBody
public Result handleParamException(ParamException e) {
log.error(e.getMessage(), e);
return Results.newFailedResult(CommonStateCode.PARAM_ERROR, e.getMessage());
}
/**
* 处理自定义异常
*/
@ExceptionHandler(AuthException.class)
@ResponseBody
public Result handleAuthException(AuthException e) {
log.error(e.getMessage(), e);
return Results.newFailedResult(CommonStateCode.AUTH_ERROR, e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public Result handleException(Exception e) {
log.error(e.getMessage(), e);
return Results.newFailedResult(CommonStateCode.BUSI_ERROR, "系统异常,请联系管理员");
}
可以看到我们返回的信息结构与Controller层是一致的,这样调用者处理接口结果就可以统一了。
国际化
项目涉及国际业务或国外用户,就要考虑国际化问题。后台代码的国际化主要是处理校验信息和异常信息。
首先是建资源文件
为资源文件命名,点+号增加语言种类
最后点OK按钮
生成如下文件
将描述信息以key value形式写入资源文件,key的命名要规范,方便开发
## VO参数校验类
vo.TakeOutDto.takeOutCheck.NOT_EMPTY=请选择提货方式
vo.InventoryDetailDto.quantity.NOT_EMPTY =请输入新增数量
## 异常类
ex.NoHandlerFoundException.001=路径不存在,请检查路径是否正确
ex.DuplicateKeyException.001=数据重复,请检查后提交
ex.Exception.001=系统异常,请联系管理员
## 参数异常
ex.ParamException.TAKEOUT.001=请先选择关联出库单
ex.ParamException.TAKEOUT.002=启动提货任务失败:没找到关联任务
ex.ParamException.TAKEOUT.003=任务{0}已删除
## 业务异常
ex.BusinessException.TAKEOUT.001=库存数量不足,库位{0},剩余数量{1}
ex.BusinessException.TAKEOUT.002=产品待提货数量不足,提货任务行{0},剩余数量{1}
校验的国际化
@Data
public class TakeOutDto {
// 提货方式
@NotNull(message = "{vo.TakeOutDto.takeOutCheck.NOT_EMPTY}")
private Integer takeOutCheck;
// 出库负责人
private Integer operator;
// 提货要求
private String comments;
// 关联提货任务单列表
private List<Integer> outstockTaskEntityIdList;
}
异常的国际化
@Override
@Transactional(rollbackFor = Exception.class)
public void discard(TakeOutVo takeOutVo) {
TakeOutEntity takeOutEntity = takeOutRepo.getById(takeOutVo.getId());
if (1 == takeOutEntity.getIsDeleted()) {
throw new ParamException(i18nUtil.getInterNationalMsg("ex.ParamException.TAKEOUT.003", takeOutVo.getId()));
}
if (takeOutEntity.getStatus() == 1 || takeOutEntity.getStatus() == 6) {
takeOutEntity.setStatus(5);
takeOutEntity.setUpdatedBy(0);
takeOutEntity.setUpdatedAt(LocalDateTime.now());
takeOutRepo.updateById(takeOutEntity);
} else {
throw new BusinessException("ex.BusinessException.TAKEOUT.003");
}
}
获取异常信息的工具类
/**
* 国际化工具类
*
* @author Alan
* @since 2022-02-08 18:00
**/
@Component
public class I18nUtil {
@Resource
private HttpServletRequest request;
private static final String ACCEPT_LANGUAGE = "Accept-Language";
private static final String FILTER_PREFIX = "#";
/**
* 获取国际化消息
*
* @param key 国际化的key
* @param args 被替换占位符的值
* 1.带#号的不是key的字符串,直接过滤#号
* 2.是key的字符串,有占位符的替换
* 3.不是key的字符串,返回字符串本身
*/
public String getInterNationalMsg(String key, Object... args) {
//带#号的不是key的字符串,直接过滤#号
if (key.startsWith(FILTER_PREFIX)) {
return key.substring(1);
}
String requestHeader = request.getHeader(ACCEPT_LANGUAGE);
//是key的字符串,有占位符的替换
try {
String propertiesName = "/I18nMessages.properties";
//如en-US,zh-CN
if (!StringUtils.isEmpty(requestHeader) && "en-US".equals(requestHeader)) {
propertiesName = "/I18nMessages_en_US.properties";
}
InputStream inputStream = this.getClass().getResourceAsStream(propertiesName);
InputStreamResource resource = new InputStreamResource(inputStream);
EncodedResource encodedResource = new EncodedResource(resource, "utf-8");
ResourcePropertySource rp = new ResourcePropertySource(encodedResource);
return MessageFormat.format((String) rp.getProperty(key), args);
} catch (Exception e) {
//不是key的字符串,返回字符串本身
return key;
}
}
}
增加拦截器统一处理接口返回值,将key替换为对应语言描述信息
/**
* 全局控制器处理
*
* @author Alan
* @since 2022-02-09 15:23
**/
@ControllerAdvice
public class ResponseBodyHandler implements ResponseBodyAdvice {
@Autowired
private I18nUtil i18nUtil;
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (body instanceof Result) {
if (200 != ((Result) body).getCode()) {
((Result) body).setMsg(i18nUtil.getInterNationalMsg(((Result) body).getMsg()));
}
}
return body;
}
}
至此,后台的国际化处理完成