[Проектная практика] Трехтактный комбинированный бокс SpringBoot, научит вас играть в элегантный внутренний интерфейс

Spring Boot

Проектное обучение для проверки истинных знаний практикой

предисловие

Бэкэнд-интерфейс примерно разделен на четыре части: адрес интерфейса (url), метод запроса интерфейса (получение, отправка и т. д.), данные запроса (запрос) и данные ответа (ответ). У каждой компании разные требования к созданию этих частей, и стандарта «должен быть лучшим» не существует, но хороший внутренний интерфейс сильно отличается от плохого внутреннего интерфейса, наиболее важным из которых является суть. это увидетьРазве это норма!Эта статья шаг за шагом продемонстрирует, как построить отличную систему внутренних интерфейсов.После того, как система будет построена, естественно, будут спецификации.В то же время будет очень легко создавать новые внутренние интерфейсы.В конце статьи вставлен адрес github демонстрации проекта, и его можно запустить после клонирования, и я сделал отправку кода для каждой записи оптимизации, вы можете четко видеть процесс улучшения проекта!

Требуемые зависимости

Здесь используется проект конфигурации SpringBoot.В этой статье основное внимание уделяется внутреннему интерфейсу, поэтому вам нужно только импортировать пакет spring-boot-starter-web:

<!--web依赖包,web应用必备-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

В этой статье также используется swagger для создания документации API и lombok для упрощения классов, но эти два не обязательны, и их можно использовать или нет.

проверка параметров

Интерфейс обычно выполняет проверку безопасности параметров (запрос данных), и важность проверки параметров, конечно, не стоит упоминать, поэтому важно, как проверять параметры.

Проверка бизнес-уровня

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

public String addUser(User user) {
     if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
         return "对象或者对象字段不能为空";
     }
     if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
         return "不能输入空字符串";
     }
     if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
         return "账号长度必须是6-11个字符";
     }
     if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
         return "密码长度必须是6-16个字符";
     }
     if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
         return "邮箱格式不正确";
     }
     // 参数校验完毕后这里就写上业务逻辑
     return "success";
 }

Конечно, ничего плохого в этом нет, и формат, и верстка аккуратные и понятные с первого взгляда, но это слишком громоздко, да еще и не эксплуатировалось, столько строк кода только для одного параметра проверка, что действительно не элегантно. Давайте улучшим его и воспользуемся двумя наборами валидаторов, Spring Validator и Hibernate Validator, для удобной проверки параметров! Два набора зависимостей Validator уже включены в упомянутые выше веб-зависимости, поэтому их можно использовать напрямую.

Валидатор + BindResult для проверки

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

@Data
public class User {
    @NotNull(message = "用户id不能为空")
    private Long id;

    @NotNull(message = "用户账号不能为空")
    @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
    private String account;

    @NotNull(message = "用户密码不能为空")
    @Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
    private String password;

    @NotNull(message = "用户邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
}

После того, как правила проверки и сообщения об ошибках настроены, вам нужно только добавить аннотацию @Valid к параметрам, которые необходимо проверить в интерфейсе, и добавить параметр BindResult для завершения проверки:

@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    private UserService userService;
    
    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
        // 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
        for (ObjectError error : bindingResult.getAllErrors()) {
            return error.getDefaultMessage();
        }
        return userService.addUser(user);
    }
}

Таким образом, когда данные запроса будут переданы в интерфейс, Валидатор автоматически завершит проверку, а результат проверки будет инкапсулирован в BindingResult.Если появится сообщение об ошибке, мы вернем его напрямую во внешний интерфейс, и код бизнес-логики вообще не будет выполняться. На данный момент код подтверждения на бизнес-уровне больше не нужен:

public String addUser(User user) {
     // 直接编写业务逻辑
     return "success";
 }

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

{
	"account": "12345678",
	"email": "123@qq.com",
	"id": 0,
	"password": "123"
}

Давайте посмотрим на данные ответа интерфейса:

Разве это не намного удобнее? Нетрудно заметить, что использование валидации Validator имеет следующие преимущества:

  1. Чтобы упростить код, большая часть кода проверки на бизнес-уровне была опущена.
  2. Он прост в использовании, и многие правила проверки могут быть легко реализованы, например, проверка формата почтового ящика. Раньше мне приходилось писать длинный список рукописных регулярных выражений, которые также подвержены ошибкам. Я могу использовать Валидатор для прямого использования аннотацию, чтобы получить его. (Есть еще аннотации правил валидации, вы можете узнать сами)
  3. Чтобы уменьшить связанность, использование Validator позволяет бизнес-уровню сосредоточиться только на бизнес-логике и отделяется от базовой логики проверки параметров.

Использование Validator + BindingResult — очень удобный и практичный способ проверки параметров, в реальной разработке есть много проектов, которые так делают, но это все равно неудобно, потому что приходится каждый раз при написании интерфейса добавлять параметр BindingResult, а потом извлеките его.Информация об ошибке возвращается во внешний интерфейс. Это немного громоздко и дублирует много кода (хотя этот дублированный код можно инкапсулировать в метод). Можем ли мы удалить шаг BindingResult? Конечно, это возможно!

Валидатор + автоматически выбрасывать исключения

Мы можем полностью удалить шаг BindingResult:

@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
    return userService.addUser(user);
}

Что происходит после его удаления? Давайте проверим это напрямую, или преднамеренно передадим в интерфейс параметр, который не соответствует правилам проверки, как раньше. В этот момент мы можем наблюдать за консолью и обнаруживать, что интерфейс поднят.MethodArgumentNotValidExceptionИсключение:

По сути, мы достигли желаемого эффекта: если проверка параметра не пройдет, следующая бизнес-логика не будет выполняться, после удаления BindingResult будет автоматически сгенерировано исключение, и бизнес-логика не будет выполняться, когда происходит исключение. Другими словами, нам вообще не нужно добавлять связанные операции, связанные с BindingResult. Но дело еще не закончено, исключение выброшено, но мы не написали код для возврата сообщения об ошибке, какие данные будут отправлены на фронтенд, если проверка параметра не удалась? Давайте посмотрим на данные ответа интерфейса после того, как только что произошло исключение:

Правильно, вся информация, связанная с объектом ошибки, напрямую передается интерфейсу! Это очень неудобно, но также очень просто решить эту проблему, которая представляет собой глобальную обработку исключений, о которой мы поговорим далее!

глобальная обработка исключений

Если проверка параметра не удалась, будет автоматически сгенерировано исключение, конечно, мы не можем вручную поймать исключение для обработки, в противном случае лучше использовать предыдущий метод BindingResult.Я не хочу вручную перехватывать это исключение, но также обрабатывать это исключение., который просто использует глобальную обработку исключений SpringBoot для достижения эффекта раз и навсегда!

основное использование

Во-первых, нам нужно создать новый класс и добавить его в этот класс@ControllerAdviceили@RestControllerAdviceАннотация, этот класс настроен как глобальный класс обработки. (Это используется в соответствии с вашим уровнем контроллера@Controllerвсе еще@RestControllerпринимать решение) Затем создайте новый метод в классе и добавьте его в метод@ExceptionHandlerАннотируйте и укажите тип исключения, которое вы хотите обработать, а затем напишите логику работы исключения в методе, и глобальная обработка исключения завершена! Давайте теперь продемонстрируем, что выбрасывается, когда проверка параметра не удалась.MethodArgumentNotValidExceptionГлобальная обработка:

@RestControllerAdvice
public class ExceptionControllerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    	// 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 然后提取错误提示信息进行返回
        return objectError.getDefaultMessage();
    }
    
}

Давайте посмотрим на данные ответа после этой ошибки проверки:

Правильно, сообщение об ошибке, которое мы сделали на этот раз, возвращается на этот раз! Мы элегантно достигаем того, чего хотим, с помощью глобальной обработки исключений! В будущем, если мы захотим написать проверку параметров интерфейса, нам нужно будет только добавить аннотацию правила проверки Validator к переменной-члену входного параметра, а затем добавить параметр к параметру.@ValidАннотация может завершить проверку, и если проверка не пройдена, сообщение об ошибке будет автоматически возвращено без какого-либо другого кода!

пользовательское исключение

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

  1. исключение перехвата (try...catch) или выдать исключение (throws)
  2. вcontrollerОбработка слоев еще не завершена.serviceобработка слоя или вdaoслой для обработки
  3. Способ обработки исключений — ничего не делать или возвращать определенные данные, и если да, то какие данные возвращать
  4. Не все исключения могут быть перехвачены заранее, что, если возникнет неперехваченное исключение?

Все вышеперечисленные проблемы могут быть решены с помощью глобальной обработки исключений.Глобальная обработка исключений также называется унифицированной обработкой исключений.Что представляют собой глобальная и унифицированная обработка?Представьте норму!Со спецификацией многие проблемы будут решены легко! Базовое использование глобальной обработки исключений уже известно всем, и в дальнейшем мы стандартизируем метод обработки исключений в проекте: пользовательское исключение. Во многих случаях нам нужно вручную генерировать исключения, например, на бизнес-уровне, когда некоторые условия не соответствуют бизнес-логике, я могу вручную генерировать исключения, чтобы инициировать откат транзакции. Самый простой способ генерировать исключения вручную —throw new RuntimeException("异常信息"), но лучше использовать пользовательский:

  1. Пользовательские исключения могут содержать больше информации, в отличие от этого, которое может содержать только строку.
  2. При разработке проекта многие люди часто отвечают за разные модули, поэтому использование пользовательских исключений может унифицировать способ отображения внешних исключений.
  3. Семантика пользовательских исключений становится все яснее и яснее, и с первого взгляда становится известно, что это исключения, создаваемые в проекте вручную.

Давайте начнем писать пользовательское исключение прямо сейчас:

@Getter //只要getter方法,无需setter
public class APIException extends RuntimeException {
    private int code;
    private String msg;

    public APIException() {
        this(1001, "接口错误");
    }

    public APIException(String msg) {
        this(1001, msg);
    }

    public APIException(int code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}

Не забудьте прямо сейчас добавить обработку нашего пользовательского исключения в глобальный класс обработки исключений:

@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
    return e.getMsg();
}

Таким образом, обработка исключений более стандартизирована, конечно, вы также можете добавитьExceptionТаким образом, независимо от того, какая аномалия возникает, мы можем заблокировать ее, а затем отправить данные во внешний интерфейс.Однако рекомендуется делать это при запуске финального проекта, что может заблокировать раскрытие информации об ошибке для front end.В разработке лучше этого не делать для удобства отладки. Теперь, когда глобальная обработка исключений и пользовательские исключения завершены, интересно, нашли ли вы проблему, то есть, когда мы выбрасываем настраиваемые исключения, глобальная обработка исключений отвечает только на сообщение об ошибке msg в исключении для внешнего интерфейса и не не Возвращается код ошибки. Это приводит к тому, о чем мы будем говорить дальше: унифицированный ответ данных.

унифицированный ответ данных

Теперь мы стандартизировали метод проверки параметров и метод обработки исключений, но мы не стандартизировали данные ответа! Например, если я хочу получить данные пейджинговой информации, в случае успешного сбора список данных будет возвращен естественным образом. Front-end разработчик вообще не знает, какие данные ответит back-end. Таким образом, унифицированные данные ответов являются обязательными во внешних и внутренних спецификациях!

настраиваемый унифицированный текст ответа

Первым шагом в ответе с унифицированными данными должна быть самостоятельная настройка класса тела ответа.Независимо от того, работает ли фон нормально или возникает исключение, формат данных ответа на внешний интерфейс не изменяется! Итак, как определить тело ответа? Вы можете обратиться к нашему пользовательскому классу исключений, а также к коду кода ответа и описанию информации ответа msg:

@Getter
public class ResultVO<T> {
    /**
     * 状态码,比如1000代表响应成功
     */
    private int code;
    /**
     * 响应信息,用来说明响应情况
     */
    private String msg;
    /**
     * 响应的具体数据
     */
    private T data;

    public ResultVO(T data) {
        this(1000, "success", data);
    }

    public ResultVO(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

Затем мы изменяем возвращаемое значение глобального обработчика исключений:

@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
    // 注意哦,这里返回类型是自定义响应体
    return new ResultVO<>(e.getCode(), "响应失败", e.getMsg());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
    // 注意哦,这里返回类型是自定义响应体
    return new ResultVO<>(1001, "参数校验失败", objectError.getDefaultMessage());
}

Давайте посмотрим, какие данные будут переданы интерфейсу, если в это время произойдет исключение:

ОК, этот ответ с информацией об исключении очень хорош, код состояния, описание ответа и данные подсказки об ошибке возвращаются во внешний интерфейс, и все исключения возвращают один и тот же формат! Здесь сделано исключение. Не забывайте, что нам также нужно изменить тип возвращаемого значения при переходе к интерфейсу. Давайте добавим интерфейс, чтобы увидеть эффект:

@GetMapping("/getUser")
public ResultVO<User> getUser() {
    User user = new User();
    user.setId(1L);
    user.setAccount("12345678");
    user.setPassword("12345678");
    user.setEmail("123@qq.com");
    
    return new ResultVO<>(user);
}

Посмотрите, что произойдет, если ответ будет возвращен правильно:

Таким образом, будь то правильный ответ или исключение, формат данных ответа унифицирован и очень стандартизирован!

Формат данных стандартизирован, но код ответа и информационное сообщение ответа еще не стандартизированы! Все обнаружили, что нет, будь то правильный ответ или ненормальный ответ, код ответа и информацию об ответе можно установить так, как вы хотите.Если 10 разработчиков напишут 10 разных кодов ответов для одного и того же типа ответа, то это унифицированное тело ответа. спецификация формата бессмысленна! Следовательно, код ответа и информация ответа должны быть стандартизированы.

перечисление кода ответа

Чтобы стандартизировать код ответа и информацию ответа в теле ответа, почти уместно использовать перечисление.Давайте теперь создадим класс перечисления кода ответа:

@Getter
public enum ResultCode {

    SUCCESS(1000, "操作成功"),

    FAILED(1001, "响应失败"),

    VALIDATE_FAILED(1002, "参数校验失败"),

    ERROR(5000, "未知错误");

    private int code;
    private String msg;

    ResultCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

}

Затем измените конструктор тела ответа, чтобы он мог принимать только перечисление кода ответа, чтобы установить код ответа и информацию ответа:

public ResultVO(T data) {
    this(ResultCode.SUCCESS, data);
}

public ResultVO(ResultCode resultCode, T data) {
    this.code = resultCode.getCode();
    this.msg = resultCode.getMsg();
    this.data = data;
}

Затем одновременно измените метод настройки кода ответа для глобальной обработки исключений:

@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
    // 注意哦,这里传递的响应码枚举
    return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
    // 注意哦,这里传递的响应码枚举
    return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}

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

Глобальная обработка данных ответов

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

Во-первых, создайте класс и аннотируйте его, чтобы сделать его глобальным классом обработки. затем унаследоватьResponseBodyAdviceИнтерфейс переписывает в нем методы, которые можно использовать для нашегоcontrollerДополнительные сведения об операциях расширения см. в коде и комментариях:

@RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"}) // 注意哦,这里要加上需要扫描的包
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
        // 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
        return !returnType.getParameterType().equals(ResultVO.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        // String类型不能直接包装,所以要进行些特别的处理
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                // 将数据包装在ResultVO里后,再转换为json字符串响应给前端
                return objectMapper.writeValueAsString(new ResultVO<>(data));
            } catch (JsonProcessingException e) {
                throw new APIException("返回String类型错误");
            }
        }
        // 将原本的数据包装在ResultVO里
        return new ResultVO<>(data);
    }
}

Эти два переопределенных метода используются вcontrollerВыполняйте операции улучшения перед возвратом данных,supportsметод возврата какtrueбудет выполнятьbeforeBodyWriteметод, поэтому, если в некоторых случаях нет необходимости выполнять операции улучшения, вы можетеsupportsсуждение в методе. Реальная работа с возвращенными данными все ещеbeforeBodyWriteВ методе мы можем напрямую обернуть данные в метод, так что каждый интерфейс не нужно оборачивать, что избавит от многих проблем.

Теперь мы можем удалить упаковку данных интерфейса, чтобы увидеть эффект:

@GetMapping("/getUser")
public User getUser() {
    User user = new User();
    user.setId(1L);
    user.setAccount("12345678");
    user.setPassword("12345678");
    user.setEmail("123@qq.com");
    // 注意哦,这里是直接返回的User类型,并没有用ResultVO进行包装
    return user;
}

Затем смотрим данные ответа:

Данные были успешно упакованы!

Уведомление:beforeBodyWriteДанные, запакованные в методе, не могут напрямую преобразовывать данные типа String, поэтому требуется специальная обработка, я не буду здесь вдаваться в подробности, если вам интересно, вы можете узнать об этом подробнее самостоятельно.

Суммировать

С тех пор была построена базовая система всего внутреннего интерфейса.

  • Удобная проверка параметров завершается валидатором + автоматическое выбрасывание исключений
  • Завершена спецификация ненормальной работы через глобальную обработку исключений + пользовательское исключение.
  • Спецификация данных ответа завершается через унифицированный ответ данных.
  • Сборка нескольких аспектов очень элегантно завершила координацию внутреннего интерфейса, что позволило разработчикам получить больше опыта, чтобы сосредоточиться на коде бизнес-логики и легко создавать внутренние интерфейсы.

Еще раз повторюсь, что нет абсолютно единого стандарта как построить систему проекта и как написать back-end интерфейс, это не значит, что он должен быть лучшим согласно этой статье, вы можете делать что хотите. ваши собственные идеи, я просто предлагаю идею!

Наконец-то поместите этот пункт здесьгитхаб-адрес, клон можно запустить прямо локально, и я представил код для каждой записи оптимизации отдельно, вы можете четко видеть процесс улучшения проекта, если это вам полезно, пожалуйста, нажмите звездочку на github, я также продолжу обновить много [практики проекта] О!

По этой статье я также написал продолжение, вы также можете его прочитать:[Практика проекта] Как элегантно расширить спецификацию, в то время как внутренний интерфейс является унифицированным и стандартизированным

Блог, Github, публичный аккаунт WeChat — все: RudeCrab, добро пожаловать на внимание! Если это полезно для вас, вы можете добавить в закладки, лайкнуть, пометить, посмотреть и поделиться ~~ Ваша поддержка - самая большая мотивация для меня, чтобы писать статьи.

Для перепечатки на WeChat, пожалуйста, обратитесь в официальный аккаунт, чтобы открыть белый список.Для перепечатки в других местах, пожалуйста, укажите оригинальный адрес и оригинального автора!