Dva načina za obradu logike validacije u Spring Boot-u: Hibernate Validator + obrada globalnih izuzetaka
Nedavno razvijam sajt za učenje zasnovan na bazi znanja, Codingmore🐱, pa mi je potrebna validacija parametara zahteva, na primer da li su prazni, ogrančenja dužine itd. Dva su moguća rešenja:
- Jedno je obrada pomoću Hibernate Validator-a
- Drugo je obrada pomoću globalnih izuzetaka
Hajde da oba načina isprobamo u praksi.
1. Hibernate Validator
Spring Boot već ima ugrađen okvir za validaciju Hibernate Validator, što se može proveriti i potvrditi na zvaničnom sajtu Spring Boot-a.
Prvi korak, uđite na zvanični sajt Spring Boot, kliknite na panel learn, a zatim na referentnu dokumentaciju.

Drugi korak, na stranici referentne dokumentacije kliknite na „verzije zavisnosti".

Treći korak, na stranici verzija zavisnosti možete videti sve zavisnosti, uključujući brojeve verzija.

PS: ako otkrijete da ne radi, možda je do konflikta verzija zavisnosti; dovoljno je ručno dodati zavisnost Hibernate Validator u fajl pom.xml.
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>Pomoću okvira za validaciju Hibernate Validator možemo direktno na polja parametara zahteva dodati anotacije i time završiti validaciju.
Kako tačno to da uradimo?
Prvi korak, na polja koja treba validirati dodajte anotacije za validaciju koje pruža Hibernate Validator.
Na primer, sada imam klasu UsersLoginParam kao parametar zahteva za prijavljivanje korisničkim imenom i lozinkom:
@Data
@ApiModel(value="Korisničko prijavljivanje", description="Tabela korisnika")
public class UsersLoginParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "Prijavno ime")
@NotBlank(message="Prijavno ime ne sme biti prazno")
private String userLogin;
@ApiModelProperty(value = "Lozinka")
@NotBlank(message="Lozinka ne sme biti prazna")
private String userPass;
}Tako se anotacijom @NotBlank može izvršiti provera praznosti za korisničko ime i lozinku. Pored anotacije @NotBlank, Hibernate Validator pruža i sledeće često korišćene anotacije:
@NotNull: anotirano polje ne sme biti null;@NotEmpty: anotirano polje ne sme biti prazno;@Min: anotirano polje mora biti veće ili jednako njegovoj vrednosti value;@Max: anotirano polje mora biti manje ili jednako njegovoj vrednosti value;@Size: anotirano polje mora biti između njegovih vrednosti min i max;@Pattern: anotirano polje mora odgovarati definisanom regularnom izrazu;@Email: anotirano polje mora odgovarati formatu e-pošte.
Drugi korak, u odgovarajući interfejs zahteva (UsersController.login()) dodajte anotaciju @Validated i ubacite parametar BindingResult.
@Controller
@Api(tags="Korisnik")
@RequestMapping("/users")
public class UsersController {
@Autowired
private IUsersService usersService;
@ApiOperation(value = "Vraća token nakon prijavljivanja")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
String token = usersService.login(users.getUserLogin(), users.getUserPass());
if (token == null) {
return ResultObject.validateFailed("Pogrešno korisničko ime ili lozinka");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ResultObject.success(tokenMap);
}
}Treći korak, za kontrolni sloj (UsersController) kreirajte aspekt koji ubacuje savet (advice) u objekat BindingResult, a zatim proverava da li postoje greške u validaciji; ako ih ima, vraća poruku o grešci validacije, u suprotnom propušta zahtev.
@Aspect
@Component
@Order(2)
public class BindingResultAspect {
@Pointcut("execution(public * com.codingmore.controller.*.*(..))")
public void BindingResult() {
}
@Around("BindingResult()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof BindingResult) {
BindingResult result = (BindingResult) arg;
if (result.hasErrors()) {
FieldError fieldError = result.getFieldError();
if(fieldError!=null){
return ResultObject.validateFailed(fieldError.getDefaultMessage());
}else{
return ResultObject.validateFailed();
}
}
}
}
return joinPoint.proceed();
}
}Ovde je u pitanju znanje iz Spring AOP-a, koje sam već objasnio u ranijem članku; putem ovog linka možete direktno otići na njega: Uvod u Spring AOP
Četvrti korak, pristupite interfejsu za prijavljivanje; kada se ne unese ni korisničko ime ni lozinka, vratiće se poruka „prijavno ime ne sme biti prazno".

U debug režimu isprobajte čitav tok rada.

Može se videti da Hibernate Validator donosi sledeće prednosti:
- Logika validacije odvojena je od poslovne logike, čime se smanjuje spregnutost programa;
- Jedinstven i standardizovan način validacije, bez potrebe za ponovnim pisanjem koda validacije.
Međutim, unosi i neke nedostatke, na primer:
- Potrebno je ubaciti objekat BindingResult u metod interfejsa zahteva, a taj objekat se ne koristi u telu metode;
- Može validirati samo vrlo jednostavnu logiku; kada su u pitanju upiti nad podacima, nemoćan je.
2. Obrada globalnih izuzetaka
Prednost korišćenja obrade globalnih izuzetaka jeste u tome što je fleksibilnija i može obraditi složeniju logiku validacije; kada validacija ne uspe, dovoljno je baciti izuzetak, a zatim ga uhvatiti i obraditi.
Prvi korak, napravite novu klasu prilagođenog izuzetka ApiException.
public class ApiException extends RuntimeException {
private IErrorCode errorCode;
public ApiException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ApiException(String message) {
super(message);
}
public ApiException(Throwable cause) {
super(cause);
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
public IErrorCode getErrorCode() {
return errorCode;
}
}Drugi korak, napravite novu klasu za obradu tvrdnji Asserts, koja pojednostavljuje korake bacanja ApiException-a.
public class Asserts {
public static void fail(String message) {
throw new ApiException(message);
}
public static void fail(IErrorCode errorCode) {
throw new ApiException(errorCode);
}
}Treći korak, napravite novu klasu za obradu globalnih izuzetaka GlobalExceptionHandler, koja parsira informacije o izuzetku i pakuje ih u jedinstveni objekat povratne vrednosti ResultObject.
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public ResultObject handle(ApiException e) {
if (e.getErrorCode() != null) {
return ResultObject.failed(e.getErrorCode());
}
return ResultObject.failed(e.getMessage());
}
}Klasa za obradu globalnih izuzetaka koristi dve anotacije, @ControllerAdvice i @ExceptionHandler.
@ControllerAdvice jeste poseban @Component (može se videti iz izvornog koda) kojim se označava klasa; metode u toj klasi označene sledeće tri anotacije: @ExceptionHandler, @InitBinder i @ModelAttribute—primeniće se na sve interfejse @Controller klasa.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
}Uloga anotacije @ExceptionHandler jeste da označi jedinstvenu obradu izuzetaka; ona može da odredi tip izuzetka koji se jedinstveno obrađuje, na primer naš prilagođeni ApiException.
Četvrti korak, tamo gde je potrebna validacija, bacite izuzetak ApiException preko klase Asserts. Uzmimo ponovo interfejs za prijavljivanje korisnika kao primer.
@Controller
@Api(tags="Korisnik")
@RequestMapping("/users")
public class UsersController {
@ApiOperation(value = "Vraća token nakon prijavljivanja")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
String token = usersService.login(users.getUserLogin(), users.getUserPass());
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ResultObject.success(tokenMap);
}
}Ovaj interfejs treba da izvrši upit nad bazom podataka radi provere ispravnosti lozinke; ako lozinka nije ispravna, baca poruku o validaciji „lozinka nije ispravna".
@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements IUsersService {
public String login(String username, String password) {
String token = null;
// Lozinku treba da šifruje klijent pre slanja
UserDetails userDetails = loadUserByUsername(username);
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
Asserts.fail("Lozinka nije ispravna");
}
// Ostali kod je izostavljen
return token;
}
}Peti korak, testirajte interfejs pomoću ApiPost-a, namerno pogrešno unesite lozinku.

Takođe možete u debug režimu isprobati čitav tok rada.

3. Rezime
U stvarnom razvoju, kombinovanjem oba pristupa nadoknađuju se međusobni nedostaci: za jednostavnu validaciju koristite Hibernate Validator, dok se za malo složeniju logiku validacije, poput one koja zahteva upit u bazi, koristi obrada globalnih izuzetaka.
Da bih deo logičke validacije u Spring Boot-u izdvojio kao zaseban demo primer, napravio sam projekat codingmore-validator i stavio ga pod projekat codingmore-learning; oni koji žele da pogledaju, mogu direktno kliknuti na sledeća dva linka.
Za izvorni kod celog projekta Codingmore kliknite na prvi link; ako želite samo logičku validaciju, kliknite na drugi.
- Adresa izvornog koda Codingmore🐱: https://github.com/itwanger/coding-more
- codingmore-validator: https://github.com/itwanger/codingmore-learning/tree/main/codingmore-validator

U suštini svaka klasa i svaki metod imaju komentare, pa se lako mogu razumeti.
