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的校验,是不是很方便。

统一异常处理

代码开发业务处理中会遇到各种情况,抛出各种异常,甚至会有我们没有处理到的异常抛到页面上,让用户看到一堆无法理解的异常信息,即不友好也不安全,所以找一个方法把异常统一处理掉,以我们规定的形式反馈出去就变得非常有必要了。

首先在项目中,我们可能定义了一堆异常类,用于在不同场景下的异常处理

springboot整合openssl springBoot整合volidator检验信息国际化_spring


异常基类

/**
 * 异常基类
 * @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层是一致的,这样调用者处理接口结果就可以统一了。

国际化

项目涉及国际业务或国外用户,就要考虑国际化问题。后台代码的国际化主要是处理校验信息和异常信息。

首先是建资源文件

springboot整合openssl springBoot整合volidator检验信息国际化_java_02


springboot整合openssl springBoot整合volidator检验信息国际化_字符串_03


为资源文件命名,点+号增加语言种类

springboot整合openssl springBoot整合volidator检验信息国际化_spring boot_04


最后点OK按钮

springboot整合openssl springBoot整合volidator检验信息国际化_springboot整合openssl_05


生成如下文件

springboot整合openssl springBoot整合volidator检验信息国际化_spring boot_06


将描述信息以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;
    }
}

至此,后台的国际化处理完成