📜 ⬆️ ⬇️

Validation in Java applications

This text is devoted to different approaches to data validation: what pitfalls can a project come across and what methods and technologies should be guided by validating data in Java applications.


Validation


I have often seen projects whose creators did not bother to choose the approach to data validation at all. The teams worked on the project under incredible pressure in the form of deadlines and vague requirements, and as a result, they simply did not have time for accurate, consistent validation. Therefore, their validation code is scattered everywhere: in Javascript snippets, screen controllers, business logic bins, domain entities, triggers, and database constraints. This code was full of if-else statements, it threw out a bunch of exceptions, and try to figure out where they have this particular piece of data validated ... As a result, as the project progresses, it becomes difficult and expensive to follow the requirements (often quite confused), and uniformity of data validation approaches.


So is there some simple and elegant way to validate data? A way that protects us from the sin of unreadability, a way that gathers all the validation logic together, and which has already been created for us by the developers of popular Java frameworks?


Yes, this method exists.


For us, the developers of the CUBA platform , it is very important that you use the best practices. We believe that the validation code should:


  1. Be reusable and follow the DRY principle;
  2. Be natural and understandable;
  3. Be located where the developer expects to see it;
  4. Be able to check data from different sources: user interface, SOAP calls, REST, etc.
  5. No problem to work in a multithreaded environment;
  6. Called within the application automatically, without the need to run a manual scan;
  7. Give the user clear, localized messages in concise dialog boxes;
  8. Follow the standards.

Let's see how this can be implemented using the example of an application written using the CUBA Platform framework. However, since CUBA is built on the basis of Spring and EclipseLink, most of the techniques used here will work on any other Java platform that supports the JPA and Bean Validation specifications.


Validation using database constraints


Perhaps the most common and obvious way to validate data is to use constraints at the database level, for example, the required flag (for fields whose value cannot be empty), string length, unique indices, etc. This method is most suitable for corporate applications, since this type of software is usually strictly focused on data processing. However, even here, developers often make mistakes, setting limits separately for each level of the application. Most often the reason lies in the distribution of responsibilities between developers.


Consider an example that most of us know, some even from our own experience ... If the specification states that there should be 10 characters in the passport number field, it is very likely that this will be checked by everyone: the database architect in DDL, the backend developer in the corresponding Entity and REST services, and finally, the developer of the UI directly on the client side. Then this requirement changes, and the field increases to 15 characters. Devopsy change the values ​​of constraints in the database, but nothing changes for the user, because on the client side the restriction is all the same ...


Any developer knows how to avoid this problem - validation should be centralized! In CUBA, such validation is in JPA annotations to entities. Based on this meta-information, CUBA Studio will generate the correct DDL script and apply the appropriate validators on the client side.


Constraints example


If the annotations change, CUBA will update the DDL scripts and generate the migration scripts, so the next time you deploy the project, the new JPA-based constraints will take effect both in the interface and in the application database.


Despite the simplicity and implementation at the database level, which gives absolute reliability to this method, the scope of JPA annotations is limited to the simplest cases that can be expressed in the DDL standard and do not include database triggers or stored procedures. So, JPA-based constraints can make an entity field unique or mandatory or set a maximum column length. You can even set a unique limit for a column combination using the @UniqueConstraint annotation. But on this, perhaps, everything.


However, in cases requiring more complex validation logic, such as checking the field for a minimum / maximum value, validating with a regular expression, or performing custom validation peculiar only to your application, the approach known as "Bean Validation" .


Bean validation


Everyone knows that it is good practice to follow standards that have a long life cycle, whose effectiveness has been proven on thousands of projects. Java Bean Validation is an approach documented in JSR 380, 349 and 303 and their applications: Hibernate Validator and Apache BVal .


Although this approach is familiar to many developers, it is often underestimated. This is an easy way to embed data validation even in legacy projects, which allows you to build checks that are understandable, simple, reliable, and as close as possible to business logic.


Using Bean Validation gives the project a lot of advantages:



When a user submits the entered information, the CUBA Platform (like some other frameworks) starts the Bean Validation automatically, so it instantly gives an error message if validation fails and we do not need to launch the validators of the beans manually.


Let's return to the example with the passport number, but this time we will supplement it with several limitations of the Person entity:



With all these checks, the Person class will look like this:


 @Listeners("passportnumber_PersonEntityListener") @NamePattern("%s|name") @Table(name = "PASSPORTNUMBER_PERSON") @Entity(name = "passportnumber$Person") @ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class}) @FraudDetectionFlag public class Person extends StandardEntity { private static final long serialVersionUID = -9150857881422152651L; @Pattern(message = "Bad formed person name: ${validatedValue}", regexp = "^[AZ][az]*(\\s(([az]{1,3})|(([az]+\\')?[AZ][az]*)))*$") @Length(min = 2) @NotNull @Column(name = "NAME", nullable = false) protected String name; @Email(message = "Email address has invalid format: ${validatedValue}", regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$") @Column(name = "EMAIL", length = 120) protected String email; @DecimalMax(message = "Person height can not exceed 300 centimeters", value = "300") @DecimalMin(message = "Person height should be positive", value = "0", inclusive = false) @Column(name = "HEIGHT") protected BigDecimal height; @NotNull @Column(name = "COUNTRY", nullable = false) protected Integer country; @NotNull @Column(name = "PASSPORT_NUMBER", nullable = false, length = 15) protected String passportNumber; ... } 

Person.java


I suppose the use of such annotations as @NotNull , @DecimalMin , @Length , @Pattern and the like is quite obvious and does not require comments. Let's take a closer look at the implementation of the @ValidPassportNumber annotation.


Our new @ValidPassportNumber verifies that Person#passportNumber matches the regexp pattern for each country specified by the Person#country field.


To get started, let's take a look at the documentation (manuals on CUBA or Hibernate will work fine), according to it, we need to mark our class with this new annotation and pass the groups parameter to it, where UiCrossFieldChecks.class means that this validation should be launched at the cross-stage validations - after checking all the individual fields, and Default.class stores the constraint in the default validation group.


The annotation description looks like this:


 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = ValidPassportNumberValidator.class) public @interface ValidPassportNumber { String message() default "Passport number is not valid"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } 

ValidPassportNumber.java


Here @Target(ElementType.TYPE) says that the purpose of this runtime annotation is the class, and @Constraint(validatedBy = … ) determines that the validation is performed by the ValidPassportNumberValidator class that implements the ConstraintValidator<...> interface. The validation code itself is in the isValid(...) method, which performs the actual check in a fairly straightforward way:


 public class ValidPassportNumberValidator implements ConstraintValidator<ValidPassportNumber, Person> { public void initialize(ValidPassportNumber constraint) { } public boolean isValid(Person person, ConstraintValidatorContext context) { if (person == null) return false; if (person.country == null || person.passportNumber == null) return false; return doPassportNumberFormatCheck(person.getCountry(), person.getPassportNumber()); } private boolean doPassportNumberFormatCheck(CountryCode country, String passportNumber) { ... } } 

ValidPassportNumberValidator.java


That's all. With the CUBA Platform, we don’t need to write anything except a line of code that will make our custom validation work and give the user error messages.
Nothing complicated, right?


Now let's check how it all works. Here, CUBA has other nishtyaki: it not only shows the user an error message, but also highlights in red the fields that have not passed the bean validation:


Wi representation


Isn't it an elegant solution? You get an adequate mapping of validation errors in the UI by adding only a couple of Java annotations to the entities of the subject area.


Summarizing the section, let us once again briefly list the advantages of the Bean Validation for entities:


  1. It is understandable and readable;
  2. Allows you to define value constraints right in entity classes;
  3. It can be customized and supplemented;
  4. Integrated into the popular ORM, and checks are run automatically before the changes are saved in the database;
  5. Some frameworks also start the validation of beans automatically when the user sends data to the UI (and if not, it’s easy to call the Validator interface manually);
  6. Bean Validation is a recognized standard, and the Internet is full of documentation on it.

But what to do if you need to set a limit on a method, constructor or REST address for validating data originating from an external system? Or if you need to declaratively check the values ​​of method parameters, without writing a tedious code with a set of if-else conditions in each method being tested?


The answer is simple: Bean Validation also applies to methods!


Validation by contract


Sometimes you need to go beyond validating the state of the data model. Many methods can benefit from automatic validation of parameters and return values. This may be necessary not only to check the data going to the REST or SOAP addresses, but also in cases where we want to register the preconditions and postconditions of method calls to make sure that the entered data was checked before the method body was executed, or that the return value is in the expected range, or we, for example, just need to declaratively describe the ranges of values ​​of input parameters to improve the readability of the code.


With the help of bean validation, constraints can be applied to the input parameters and return values ​​of methods and constructors to check the preconditions and postconditions of their calls in any Java class. There are several advantages to this path over traditional methods for validating parameters and return values:


  1. There is no need to carry out manual checks in an imperative style (for example, by throwing out IllegalArgumentException and the like). You can define constraints declaratively, and make the code more understandable and expressive;
  2. Constraints can be configured, reused and configured: no need to write validation logic for each check. Less code - less bugs.
  3. If the class, the return value of the method, or its parameter are annotated with @Validated , then the platform will automatically perform the checks each time the method is called.
  4. If the executable is annotated with @Documented , its preconditions and postconditions will be included in the generated JavaDoc.

Using 'contract validation' we get clear, compact and easily supported code.


For an example, let's look at the interface of a REST controller in a CUBA application. The PersonApiService interface allows you to get a list of people from a database using the getPersons() method and add a new person using the call addNewPerson(...) .


And do not forget that bean validation is inherited! In other words, if we annotate a certain class, or field, or method, then the same validation annotation will be applied to all classes that inherit this class or implement this interface.


 @Validated public interface PersonApiService { String NAME = "passportnumber_PersonApiService"; @NotNull @Valid @RequiredView("_local") List<Person> getPersons(); void addNewPerson( @NotNull @Length(min = 2, max = 255) @Pattern(message = "Bad formed person name: ${validatedValue}", regexp = "^[AZ][az]*(\\s(([az]{1,3})|(([az]+\\')?[AZ][az]*)))*$") String name, @DecimalMax(message = "Person height can not exceed 300 cm", value = "300") @DecimalMin(message = "Person height should be positive", value = "0", inclusive = false) BigDecimal height, @NotNull CountryCode country, @NotNull String passportNumber ); } 

PersonApiService.java


Is this code fragment clear enough?
_ (Except for the @RequiredView(“_local”) annotation, specific to the CUBA Platform and checking that the Person object returned contains all the fields from the PASSPORTNUMBER_PERSON table) ._


The @Valid specifies that every object in the collection returned by the getPersons() method must also be validated to meet the constraints of the Person class.


In the CUBA application, these methods are available at the following addresses:



Open the Postman application and make sure that validation works as it should:


Postman app


As you may have noticed, in the example above, the passport number is not validated. This is because this field requires cross-validation of the parameters of the addNewPerson method, since the choice of a regular expression pattern for validating passportNumber depends on the value of the country field. This cross-validation is a complete analogue of class-level entity constraints!


Cross validation of parameters is supported by JSR 349 ​​and 380. You can read the hibernate documentation to learn how to implement your own cross validation of class / interface methods.


Outside bean validation


There is no perfection in the world, so bean validation has its drawbacks and limitations:


  1. Sometimes we just need to check the status of a complex object graph before saving changes to the database. For example, you need to make sure that all elements of the order of the buyer are placed in one package. This is quite a difficult operation, and to carry it out every time the user adds new items to the order is not the best idea. Therefore, such a check may be needed only once: before saving the Order object and its sub-objects OrderItem in the database.
  2. Some checks need to be carried out within a transaction. For example, an electronic store system must check whether there are enough copies of a product in stock to fulfill an order before it is committed to the database. Such verification can only be done within a transaction, since the system is multi-threaded and the quantity of goods in stock may change at any time.

The CUBA Platform offers two mechanisms for data validation to commit, which are called entity listeners and transaction listeners . Consider them in more detail.


Entity listemers


Entity listeners in CUBA are very similar to PreInsertEvent , PreUpdateEvent and PredDeleteEvent listeners that JPA offers to the developer. Both mechanisms allow you to check entity objects before and after they are stored in the database.


In CUBA it is easy to create and connect an entity listener, for this you need two things:


  1. Create a managed bean that implements one of the entity listener interfaces. Three interfaces are important for validation:
    BeforeDeleteEntityListener<T> ,
    BeforeInsertEntityListener<T> ,
    BeforeUpdateEntityListener<T>
  2. Add the @Listeners annotation to the entity object that you @Listeners to monitor.

And that's all.


Compared to the JPA standard (JSR 338, Section 3.5), the CUBA Platform listener interfaces are typed, so you don’t need to cast the argument of type Object to an entity type to start working with it. The CUBA platform adds to bound entities or caller EntityManager the ability to load and modify other entities. All of these changes will also trigger the corresponding entity listener.


Also, the CUBA platform supports "soft deletion" , an approach where instead of actually deleting records from the database, they are only marked as deleted and become inaccessible for normal use. So, for soft deletion, the platform calls BeforeDeleteEntityListener / AfterDeleteEntityListener , while standard implementations would call PreUpdate / PostUpdate .


Let's look at an example. Here, the Event listener bean connects to an entity class with just one line of code: the @Listeners annotation, which takes the name of the listener class:


 @Listeners("passportnumber_PersonEntityListener") @NamePattern("%s|name") @Table(name = "PASSPORTNUMBER_PERSON") @Entity(name = "passportnumber$Person") @ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class}) @FraudDetectionFlag public class Person extends StandardEntity { ... } 

Person.java


The listener implementation itself looks like this:


 /** * Checks that there are no other persons with the same * passport number and country code * Ignores spaces in the passport number for the check. * So numbers "12 45 768007" and "1245 768007" and "1245768007" * are the same for the validation purposes. */ @Component("passportnumber_PersonEntityListener") public class PersonEntityListener implements BeforeDeleteEntityListener<Person>, BeforeInsertEntityListener<Person>, BeforeUpdateEntityListener<Person> { @Override public void onBeforeDelete(Person person, EntityManager entityManager) { if (!checkPassportIsUnique(person.getPassportNumber(), person.getCountry(), entityManager)) { throw new ValidationException( "Passport and country code combination isn't unique"); } } @Override public void onBeforeInsert(Person person, EntityManager entityManager) { // use entity argument to validate the Person object // entityManager could be used to access database // if you need to check the data // throw ValidationException object if validation check failed if (!checkPassportIsUnique(person.getPassportNumber(), person.getCountry(), entityManager)) throw new ValidationException( "Passport and country code combination isn't unique"); } @Override public void onBeforeUpdate(Person person, EntityManager entityManager) { if (!checkPassportIsUnique(person.getPassportNumber(), person.getCountry(), entityManager)) throw new ValidationException( "Passport and country code combination isn't unique"); } ... } 

PersonEntityListener.java


Entity listeners are a great choice if:



Transaction listeners


CUBA transaction listeners also act in the context of transactions, but, compared to entity listeners, they are invoked for each database transaction.


These gives them super strength:



But their shortcomings also determine this:



So, transaction listeners are a good solution when you need to inspect different types of entities using the same algorithm, for example, checking all data for cyber fraud with a single service that serves all your business objects.


You shall not pass!


Take a look at a sample that checks whether an entity has an @FraudDetectionFlag annotation, and, if there is, launches a fraud detector. I repeat: keep in mind that this method is called in the system before committing each transaction of the database , so the code should try to check as few objects as possible.


 @Component("passportnumber_ApplicationTransactionListener") public class ApplicationTransactionListener implements BeforeCommitTransactionListener { private Logger log = LoggerFactory.getLogger(ApplicationTransactionListener.class); @Override public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) { for (Entity entity : managedEntities) { if (entity instanceof StandardEntity && !((StandardEntity) entity).isDeleted() && entity.getClass().isAnnotationPresent(FraudDetectionFlag.class) && !fraudDetectorFeedAndFastCheck(entity)) { logFraudDetectionFailure(log, entity); String msg = String.format( "Fraud detection failure in '%s' with id = '%s'", entity.getClass().getSimpleName(), entity.getId()); throw new ValidationException(msg); } } } ... } 

ApplicationTransactionListener.java


To turn into a transaction listener, a managed bean must implement the BeforeCommitTransactionListener interface and the beforeCommit method. Transaction listeners are automatically linked when the application starts. CUBA registers all classes that implement BeforeCommitTransactionListener or AfterCompleteTransactionListener as transaction listeners.


Conclusion


Bean validation (JPA 303, 349, and 980) is an approach that can serve as a reliable basis for 95% of the validation cases found in a corporate project. The main advantage of this approach is that most of the validation logic is concentrated directly in the domain model classes. Therefore, it is easy to find, easy to read and easy to maintain. Spring, CUBA, and many other libraries support these standards and automatically perform validation checks during data acquisition at the UI layer, calling validated methods, or the process of storing data through ORM, so from the developer’s point of view, Bean validation often looks like magic.


Some software developers consider validation at the class level of the subject model as unnatural and too complex, they say that data verification at the UI level is a fairly effective strategy. However, I believe that multiple validation points in UI components and controllers are not the most rational approach. , , , , , listener' .


, , :


  1. JPA , , DDL.
  2. Bean Validation — , , , . , .
  3. bean validation, . , , REST.
  4. Entity listeners: , Bean Validation, . , . Hibernate .
  5. Transaction listeners — , , . , , .

PS: , Java, , , .




useful links



')

Source: https://habr.com/ru/post/427543/


All Articles