В единия от страничните проекти използваме REST за комуникация между фронтенда (Vue) и бекенда (Spring Boot). Приложението е сравнително просто и пиша бекенда сам и за документация на REST-а използвам Swagger. Това значително услеснява имплементацията на фронтенда откъм информация за endpoint-и и кой какво може да очаква като заявка/отговор.
Тъй като в процеса на работа постоянно изникват нови възможни отговори за грешка, които касаят само клиента, счетох за ненужно (i.e. домързя ме) да създавам нови Exception-и за всяка.
Механизмът, на който се спрях за връщане на грешките към потребителския интерфейс, е сравнително прост и не е нищо ново – при грешка в бекенда към клиента се връща DTO с цифров код и пояснителен текст. Пояснителният текст служи само за улеснение на пишещия фронтенда. Локализацията на грешките, ако се наложи да има такава, ще се случва във Vue приложението според цифровия код.
В Spring това се имплементира изключително лесно. При неправилно действие (няма достъп, липсва задължително поле и т.н.) се хвърля Exception (в случая – ErrorResponseException
), който се прихваща от @ExceptionHandler
и към клиента се връща DTO-то с нужния код и текст. За имплементацията на този механизъм са нужни няколко основни неща:
- Номенклатура на кодовете на грешки и клас, описващ формата на отговора, който очаква Vue приложението,
- Някой да прихване Exception-а,
- Exception за прихващане.
Номенклатурата е обикновено POJO със статични методи за всяка грешка:
import lombok.Data;
@Data
@AllArgsConstructor
public class ErrorResponseDTO {
private Integer code;
private String message;
// 500
public static ErrorResponseDTO generalError(Throwable t) {
return new ErrorResponseDTO(500, "I can't even: " + t.getMessage());
}
// 1401 - JWT token expired
public static ErrorResponseDTO tokenExpired() {
return new ErrorResponseDTO(1401, "Token expired");
}
// 1402 - Invalid JWT token
public static ErrorResponseDTO invalidToken() {
return new ErrorResponseDTO(1402, "Invalid token");
}
// ...
// 1409 - User is not active
public static ErrorResponseDTO userIsNotActive() {
return new ErrorResponseDTO(1409, "User is not active");
}
// ...
// Още много статични методи, описващи останалите грешки
// ...
}
Горното може да се реализира по много начини. Засега това ме устройва. Не смятам, че проектът ще стане достатъчно сложен, за да оправдае разделянето на тази номенклатура от DTO-то или категоризиране на грешките в отделни класове, пакети, Exception-и и т.н.
Прихващането на Exception-а става с @ControllerAdvice
и @ExceptionHandler
:
@ControllerAdvice
public class RestExceptionHandler {
// ...
@ExceptionHandler(ErrorResponseException.class)
@ResponseStatus(HttpStatus.PRECONDITION_FAILED)
@ResponseBody
public ErrorResponseDTO processErrorResponseException(ErrorResponseException
e) {
return e.getErrorResponseDTO();
}
// ...
// Други handler-и за други exception-и
// ...
}
Exception-ът е обикновен наследник на RuntimeException, който съдържа в себе си DTO-то:
public class ErrorResponseException extends RuntimeException {
private ErrorResponseDTO errorResponseDTO;
public ErrorResponseException(ErrorResponseDTO errorResponseDTO) {
this.errorResponseDTO = errorResponseDTO;
}
public ErrorResponseDTO getErrorResponseDTO() {
return errorResponseDTO;
}
}
С описаното дотук връщането на грешка става просто. Например, в Service-а за генериране на JWT токен има простата проверка дали конкретният потребител е активен. Ако не е, няма смисъл да му генерирам токен:
if (userData.getUserStatus() != UserStatus.ACTIVE) {
throw new ErrorResponseException(ErrorResponseDTO.userIsNotActive());
}
Така при неактивен потребител към извикващия клиент се връща този JSON:
{
"code": 1409,
"message": "User is not active"
}
Логиката за потребителския интерфейс е изцяло в клиентското приложението. Дали ще се покаже директно върнатият текст или според кода на грешката ще се случи нещо, няма значение. Целта е пълно отделяне на логиката по обработване на грешката (ако така се превежда decoupling). Няма значение дали клиентът е Vue, Android или Swing приложение – отговорността е негова.
Всичко е хубаво до момента, в който не се наложи друг човек да работи с това API и се започне едно питане “Ама тоя код какво означава”. Най-добрият вариант, разбира се, е да има подробна документация с всички кодове и условията, при които се връщат, но в конкретния случай това не е оправдано. А и ако не бях мързел, нямаше да съм програмист.
Можеше вместо рефлексия да използвам анотациите, предоставени от Swagger за този случай (@ApiResponses
и @ApiResponse
) върху всеки метод на всеки контролер в приложението.
Понеже рефлексията ми е интересна, се спрях на вариант с endpoint, който връща JSON с всички възможни грешки, които могат да се върнат към REST клиента. Тъй като всички грешки са дефинирани като статични методи на класа, който ги описва, алгоритъмът е тривиален:
- Вземам всички статични методи, декларирани в този клас,
- Викам ги един по един,
- Записвам резултатите в списък,
- Връщам списъка.
@RestController
@Slf4j
public class ErrorListController {
@GetMapping(path = "/errors-list")
public List<ErrorResponseDTO> getErrorsList() {
List<ErrorResponseDTO> result = new ArrayList<>();
// Взимаме всички методи
Method[] methods = ErrorResponseDTO.class.getMethods();
for (Method method : methods) {
try {
// Искаме само статичните методи
if (Modifier.isStatic(method.getModifiers())) {
Method declaredMethod = ErrorResponseDTO.class.getDeclaredMethod(method.getName());
// Викаме метода
ErrorResponseDTO invoke = (ErrorResponseDTO) declaredMethod.invoke(null);
// Добавяме резултата към отговора
result.add(invoke);
}
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
log.debug("Ignoring exception \"{}\": {}", e.getMessage(), e);
}
}
return result;
}
}
Адресът на този контролер (/errors-list
) е разрешен в WebSecurityConfigurerAdapter
-а само при определени условия и не се вижда в “продукционната” среда. Отговорът изглежда долу-горе така:
[
{
"code": 1401,
"message": "Token expired"
},
{
"code": 1402,
"message": "Invalid token"
},
{
"code": ...,
"message": "..."
},
{
"code": 1409,
"message": "User is not active"
}
]
Така има винаги актуален списък с цялата номенклатура на възможните кодове на грешки. Смятам, че с малко повече заигравка ще мога да докарам и информация откъде може да се върне даден код за грешка, но към момента това ми върши работа.
Leave a Reply