Платежная система - унифицированная обработка глобальных исключений и внешних возвратов

Java

предисловие

За многолетнюю практику я участвовал в проектировании многих систем. С точки зрения удовлетворения потребностей бизнеса систему, которая может быстро поддерживать развитие бизнеса, можно назвать «хорошей» системой. В конце концов, именно бизнес создает ценность, и если нет бизнес-драйвера, инженерам будет очень тяжело освоить навыки убийства драконов без драконов. Как отличный программист, я считаю, что все надеются, что разрабатываемая ими система будет простой в обслуживании и более надежной. Конечно, это просто идеализм. Не говоря уже о том, что интернет-индустрия быстро меняется, только сжатые сроки строительства иdeadlineПрограммирование требует, чтобы многие инженеры отказались от своей одержимости ремонтопригодностью кода. Что касается личного опыта автора, то принято утром получать требуемый документ, а вечером выходить в интернет. Тем не менее, я все же надеюсь поговорить с личной точки зрения о том, какое удобство могут принести некоторые простые и удобные привычки в обслуживании программы. Эта статья начинается с неправильного использования и рассказывает о неприятном вкусе, вызванном неправильным использованием.

Проверенное/непроверенное исключение

существуетJavaсуществуют вRuntimeExceptionа такжеException. типичный какNullPointerExceptionпринадлежатьRuntimeException, эти исключения не нужно перехватывать разработчику, и они автоматически создаются после запуска во время выполнения. в то же время,RuntimeExceptionТакже известное как непроверенное исключение, как следует из названия, оно не проверяется во время компиляции. КромеRuntimeExceptionВсе другие исключения называются проверенными исключениями, которые необходимо принудительно обрабатывать, бросать или перехватывать во время компиляции.ClassNotFoundException,InterruptedException.

Если вы чувствуете, что здесь есть исключение, объявите его

Я видел много такого кода:

PayOrder selectByOrderId(Long orderId) throw Exception;

Всегда кажется, что я написал это из-за скептицизма по поводу сетевых подключений/баз данных.SQLГенерирует исключение и хочет, чтобы восходящий поток обработал это исключение. Это называется защитным программированием, но на самом деле оно добавляет много проблем сопровождению программы. Прежде всего, поскольку генерация этого исключения носит очень общий характер, вызывающая сторона не знает, что думает человек, выбрасывавший исключение в данный момент, и может только принудительно перехватить исключение во внешнем слое. Если вызывающий объект верхнего уровня не хочет обрабатывать исключение, он продолжит генерировать его на верхний уровень.После обработки несколько раз самый внешний программный блок не знает, какое исключение было создано. Возможно, многие друзья скажут, что мой стиль программирования заключается в том, что когда у других есть исключения, они могут их обработать, и их нельзя обработать до того, как они будут выброшены на верхний уровень. Тогда я хочу сказать, ты потрясающий. Однако в проекте не один разработчик, у каждого свои привычки в программировании. Некоторым друзьям просто нравится выбрасывать исключения на внешний уровень и продолжать смущать вызывающую сторону верхнего уровня. Конечно, это не значит, что генерация исключений — это плохо. Разумное использование исключений может сделать структуру программы более понятной, а семантику — более понятной. Вызывающим объектам высокого уровня также удобно обрабатывать различные исключения, а не просто беспомощный захват.Exception.

Поймать с или без

Продолжая выше, если кто-то выдает проверенное исключение с самого низа. В такой системе, подумайте, что люди будут делать? Это легко, просто лови. Однако, если нижний уровень бросает非受检异常то естьRuntimeExceptionШерстяная ткань? Извините, как только кто-то из команды сделает это,在没有全局异常处理的情况下, опытные разработчики предпочтут перехватывать это исключение на внешнем уровне, в то время как разработчики, которым не хватает опыта или которые не знакомы с системой, естественно, не перехватят это исключение. Тогда в контейнер будет выброшена такая ошибка, что это значит? Предположим, вы возвращаетеJSONFormat, если его кинуть в контейнер, возвращаемый контент — это сообщение об ошибке программы. Другие вызывающие объекты не смогут проанализировать это возвращаемое значение, что определенно не так.

С этого момента вы можете видеть, что все внешние программы имеют уродливыеtry catchблокируется независимо от того, выдает ли вызывающая программа исключение.

немного практики

исключения и перечисления

существуетSpringВ контейнерах декларативные транзакции обычно используются для управления транзакциями базы данных. существует@TransactionВ процессе использования необходимо указать соответствующий тип исключения. Автор столкнулся со многими проектами, исключением является откатRuntimeException. Спросите почему и ответьте даExceptionНе откатится, что собственно и вызвано неправильной настройкой. Использование вышеперечисленного было проанализированоRuntimeExceptionНедостатки, то давайте поговорим об этом сейчасException. Это обязательное исключение, которое необходимо захватить, имеет строгую семантику с пользовательскими исключениями, что удобно для высокоуровневого гибкого выбора и обработки.Как правило, оно широко используется в инженерии. Но если вы определите новый класс для каждого исключения, это будет очень многословно. Распространенной практикой является вынесение бизнес-суждений с помощью значений перечисления и исключений. следующим образом:

public enum ResultCodeEnum {
    /**
     * 成功
     */
    SUCCESS("SUCCESS", "ok"),
    /**
     * 操作失败
     */
    FAIL("FAIL", "操作失败"),
    /**
     * 系统错误
     */
    ERROR("ERROR", "系统繁忙,请稍后再试。"),
    /**
     * 验签失败
     */
    VERIFY_FAILED("VERIFY_FAILED", "验签失败"),
    /**
     * 缺少参数
     */
    LACK_PARAM("LACK_PARAM", "缺少参数"),

    ;


    @Getter
    private String code;
    @Getter
    private String msg;
    private ResultCodeEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

Исключение с параметрами перечисления:

public class BusinessException extends Exception {

    private static final long serialVersionUID = -121219158129626814L;
    @Getter
    private ResultCodeEnum resultCode;
    @Getter
    private String msg;
    public BusinessException() {
    }
    public BusinessException(ResultCodeEnum rsCode) {
        super(rsCode.getCode() + ":" + rsCode.getMsg());
        this.resultCode = rsCode;
        this.msg = rsCode.getMsg();
    }
    public BusinessException(ResultCodeEnum rsCode, String message) {
        super(rsCode.getCode() + ":" + message);
        this.resultCode = rsCode;
        this.msg = message;
    }
    public BusinessException(ResultCodeEnum rsCode, Throwable cause) {
        super(rsCode.getCode() + ":" + rsCode.getMsg(), cause);
        this.resultCode = rsCode;
        this.msg = rsCode.getMsg();
    }
    public BusinessException(ResultCodeEnum rsCode, String message, Throwable cause) {
        super(rsCode.getCode() + ":" + message, cause);
        this.resultCode = rsCode;
        this.msg = message;
    }
}

Таким образом, его можно использовать там, где необходимо создать исключение.

PayTypeEnum payTypeEnum = PayTypeEnum.toEumByName(payRequestDTO.getPayType());
if (payTypeEnum == null) {
    throw new BusinessException(ResultCodeEnum.INVALID_PAY_TYPE);
}

Таким образом, внешний уровень должен перехватывать это исключение, которое может быть основано наResultCodeEnumзначение, чтобы отличить бизнес и выполнить соответствующую обработку.

Единое возвращаемое значение

Вообще говоря, исходящие предложения, предоставляемые службой внешнему миру, унифицированы. можно использоватьpayloadШаблон оборачивает возвращаемый результат, вы можете не понять, посмотрите на следующий класс:

public class ResultMessageVO<T> {

    public static final String SUCCESS = "success";

    public static final String ERROR = "error";
    private String status;      //状态

    private String message;     //消息

    private T data;     //返回的数据

    ...
}

Таким образом, если вы используете аспекты или другие глобальные механизмы обработки исключений, легко стандартизировать возврат. Возьмем пример аспекта, который проверяет параметры:

@Aspect
@Component
@Slf4j
public class ValidationAspect {
    @Around("execution(* io.github.pleuvoir.gateway..*.*(..))")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        Object[] args = point.getArgs();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            if (parameters[i].isAnnotationPresent(Valid.class)) {
                Object val = args[i];
                ValidationResult validationResult = HibernateValidatorUtils.validateEntity(val);
                if (validationResult.isHasErrors()) {
                    return ResultMessageVO.fail(ResultCodeEnum.PARAM_ERROR, validationResult.getErrorMessageOneway());
                }
            }
        }
        return point.proceed();
    }
}

Потому что мы возвращаемResultMessageVOСледовательно, в аспекте может выполняться унифицированная обработка, в противном случае каждый метод должен выполнять проверку параметров отдельно, что является преимуществом унифицированного возвращаемого значения.

Конечно, если есть унифицированное исключение, с ним лучше иметь дело из-за существования этого унифицированного возвращаемого значения:

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResultMessageVO<?> exception(HttpServletRequest request, Exception e){
        if (e instanceof NoHandlerFoundException) {
            log.error("页面不存在:{}", e.getMessage());
            return new ResultMessageVO(ResultCodeEnum.ERROR, "页面不存在");
        } else if (e instanceof BindException) {
            log.error("参数格式错误:{} url:{}", e.getMessage(), request.getRequestURI());
            return new ResultMessageVO(ResultCodeEnum.INVALID_ARGUMENTS, "参数格式错误");
        } else if (e instanceof HttpRequestMethodNotSupportedException){
            log.error("不支持的请求方式:{} url:{}", e.getMessage(), request.getRequestURI());
            return new ResultMessageVO(ResultCodeEnum.ERROR, "不支持的请求方式");
        } else if (e instanceof BusinessException) {
            BusinessException exception = (BusinessException) e;
            log.warn("业务异常:{} url: {}", exception.getMsg(), request.getRequestURI());
            return new ResultMessageVO<>(exception.getResultCodeEnum(), exception.getMsg());
        } else {
            log.error("系统异常:{} \t\r\n url: {} \t\r\n header: {} \t\r\n params: {} \t\r\n body: {}",
                    e.getMessage(), request.getRequestURI(), RequestUtil.getHeaders(request), RequestUtil.getParameterMap(request), getBody(request), e);
            return new ResultMessageVO<>(ResultCodeEnum.ERROR);
        }
    }
    private String getBody(HttpServletRequest request) {
        String body = StringUtils.EMPTY;
        try {
            body = RequestUtil.getBody(request);
        } catch (IOException e) {
            log.error("打印系统异常日志时,读取请求body失败,url:{}", request.getRequestURI(), e);
        }
        return body;
    }
}

Благодаря сочетанию двух вышеуказанных шагов мы успешно использовали проверенное исключение и перечисление для завершения рационального использования исключений, а затем сотрудничали с глобальной обработкой исключений для завершения окончательного результата. Таким образом, программе нужно только захватитьBusinessExceptionВот и все. Успешно устранены отвратительные фрагменты кода.

послесловие

Эта статья является обобщением моего опыта.Неизбежны упущения или даже ошибки.Если есть необоснованные или недостаточные,поправьте меня. Кроме того, я надеюсь, что вы сможете рассказать о том, как вы используете исключения в своих проектах, и поучиться друг у друга.