Validation in Java applications
This text focuses on different approaches to data validation: which pitfalls a project can stumble upon and which methods and technologies should be used to validate data in Java applications.
I often saw projects whose creators did not bother at all with the choice of approach to data validation. 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 hard 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 unreadableness, 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 can use the best practices. We believe that the validation code should:
- Be reusable and follow the DRY principle;
- Be natural and understandable;
- Be located where the developer expects to see it;
- Be able to check data from different sources: user interface, SOAP calls, REST, etc.
- No problem to work in a multithreaded environment;
- Called within the application automatically, without the need to start the checks manually;
- Give the user clear, localized messages in concise dialog boxes;
- 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), the 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 are familiar with, 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 it 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.
If the annotations change, CUBA will update the DDL scripts and generate 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 annotation @UniqueConstraint
. But on this, perhaps, everything.
However, in cases requiring more complex validation logic, such as checking the field for a minimum / maximum value, validation using 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:
- The validation logic is located next to the subject area: the definition of constraints for the fields and methods of the bean occurs in a natural and truly object-oriented manner.
- Bean Validation standard gives us dozens of validation annotations out of the box , such as:
@NotNull
,@Size
,@Min
,@Max
,@Pattern
,@Email
,@Past
, not quite standard@URL
,@Length
, powerful@ScriptAssert
, and many others. - The standard does not limit us to finished annotations and allows you to create your own. We can also create a new annotation by combining several others, or define it using a separate Java class as a validator.
For example, in the example above, we can set the class level annotation@ValidPassportNumber
to verify that the passport number matches the format depending on the field valuecountry
. - Restrictions can be placed not only on fields or classes, but also on methods and their parameters. This approach is called “validation by contract” and will be discussed later.
When a user submits the information entered, 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 us return to the example with the passport number, but this time we will supplement it with several limitations of the Person entity:
- The field
name
must consist of 2 or more characters and must be valid. (As you can see, regexp is not easy, but "Charles Ogier de Batz de Castelmore Comte d'Artagnan" will be tested, but "R2D2" is not); height
(height) should be in the following interval:0 < height <= 300
cm;- The field
email
must contain a string corresponding to the format of a valid email.
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})
@FraudDetectionFlagpublicclassPersonextendsStandardEntity{
privatestaticfinallong serialVersionUID = -9150857881422152651L;
@Pattern(message = "Bad formed person name: ${validatedValue}",
regexp = "^[A-Z][a-z]*(\\s(([a-z]{1,3})|(([a-z]+\\')?[A-Z][a-z]*)))*$")
@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;
...
}
I believe the use of such annotations as @NotNull
, @DecimalMin
, @Length
, @Pattern
and the like, it is clear and requires no comment. Let's take a closer look at the implementation of annotations @ValidPassportNumber
.
Our new one @ValidPassportNumber
checks that it Person#passportNumber
matches the regexp pattern for each country specified by the field Person#country
.
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 a parameter to it groups
, where it UiCrossFieldChecks.class
means that this validation should be started at the cross-validation stage - after checking all individual fields, and Default.class
stores the constraint in the default validation group.
An 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 {};
}
Here it @Target(ElementType.TYPE)
says that the purpose of this runtime annotation is the class, and @Constraint(validatedBy = … )
determines that the check is performed by the class ValidPassportNumberValidator
that implements the interface ConstraintValidator<...>
. The validation code itself is in a method isValid(...)
that performs the actual check in a fairly straightforward way:
publicclassValidPassportNumberValidatorimplementsConstraintValidator<ValidPassportNumber, Person> {
publicvoidinitialize(ValidPassportNumber constraint){
}
publicbooleanisValid(Person person, ConstraintValidatorContext context){
if (person == null)
returnfalse;
if (person.country == null || person.passportNumber == null)
returnfalse;
return doPassportNumberFormatCheck(person.getCountry(),
person.getPassportNumber());
}
privatebooleandoPassportNumberFormatCheck(CountryCode country,
String passportNumber){
...
}
}
ValidPassportNumberValidator.java
That's all. With the CUBA Platform, we do not 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:
Isn't it an elegant solution? You get an adequate display 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 briefly list the advantages of the Bean Validation for entities:
- It is understandable and readable;
- Allows you to define constraints on values right in entity classes;
- It can be customized and supplemented;
- Integrated into the popular ORM, and checks are run automatically before the changes are saved in the database;
- Some frameworks also start validating bins automatically when the user sends data to the UI (and if not, it’s easy to call the interface
Validator
manually); - 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 is applicable 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 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:
- 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; - Constraints can be configured, reused and configured: no need to write validation logic for each check. Less code - less bugs.
- If the class, the return value of the method or its parameter are annotated
@Validated
, then the checks will be automatically performed by the platform each time the method is called. - If the module being executed is annotated
@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 interface PersonApiService
allows you to get a list of people from the database using the method getPersons()
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.
@ValidatedpublicinterfacePersonApiService{
String NAME = "passportnumber_PersonApiService";
@NotNull@Valid@RequiredView("_local")
List<Person> getPersons();
voidaddNewPerson(
@NotNull
@Length(min = 2, max = 255)
@Pattern(message = "Bad formed person name: ${validatedValue}",
regexp = "^[A-Z][a-z]*(\\s(([a-z]{1,3})|(([a-z]+\\')?[A-Z][a-z]*)))*$")
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
);
}
Is this code fragment clear enough?
_ (Except for the annotation @RequiredView(“_local”)
specific to the CUBA Platform and verifying that the returned object Person
contains all the fields from the table PASSPORTNUMBER_PERSON
) ._
The annotation @Valid
specifies that each collection object returned by the method getPersons()
must also be validated to comply with the class constraints Person
.
In the CUBA application, these methods are available at the following addresses:
- / app / rest / v2 / services / passportnumber_PersonApiService / getPersons
- / app / rest / v2 / services / passportnumber_PersonApiService / addNewPerson
Open the Postman application and make sure that validation works as it should:
As you may have noticed, in the example above the passport number is not validated. This is because this field requires cross-validation of method parameters addNewPerson
, since the choice of a regular expression pattern for validation passportNumber
depends on the field value country
. 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:
- 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 it is not the best idea to carry it out every time the user adds new items to the order. Therefore, such a check may be needed only once: before saving the object
Order
and its sub-objectsOrderItem
in the database. - Some checks need to be carried out within a transaction. For example, an electronic store system should 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, 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:
- Create a managed bean that implements one of the entity listener interfaces. Validation is important 3 interface:
BeforeDeleteEntityListener<T>
,BeforeInsertEntityListener<T>
,BeforeUpdateEntityListener<T>
- Add annotation
@Listeners
to the entity object that you want to track.
And that's all.
Compared to the JPA standard (JSR 338, Section 3.5), the CUBA Platform listener interfaces are typed, so you do not need to cast Object
the type argument to an entity type to start working with it. The CUBA platform adds the ability of related entities or calling EntityManager 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 unavailable for normal use. So, for soft deletion, the platform calls listeners BeforeDeleteEntityListener
/ AfterDeleteEntityListener
while standard implementations would call listeners PreUpdate
/ PostUpdate
.
Let's look at an example. Here, the Event listener bean connects to an entity class with just one line of code: an annotation @Listeners
that 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})
@FraudDetectionFlagpublicclassPersonextendsStandardEntity{
...
}
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")
publicclassPersonEntityListenerimplementsBeforeDeleteEntityListener<Person>,
BeforeInsertEntityListener<Person>,
BeforeUpdateEntityListener<Person> {
@OverridepublicvoidonBeforeDelete(Person person, EntityManager entityManager){
if (!checkPassportIsUnique(person.getPassportNumber(),
person.getCountry(), entityManager)) {
thrownew ValidationException(
"Passport and country code combination isn't unique");
}
}
@OverridepublicvoidonBeforeInsert(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 failedif (!checkPassportIsUnique(person.getPassportNumber(),
person.getCountry(), entityManager))
thrownew ValidationException(
"Passport and country code combination isn't unique");
}
@OverridepublicvoidonBeforeUpdate(Person person, EntityManager entityManager){
if (!checkPassportIsUnique(person.getPassportNumber(),
person.getCountry(), entityManager))
thrownew ValidationException(
"Passport and country code combination isn't unique");
}
...
}
Entity listeners are a great choice if:
- It is necessary to check the data inside the transaction before the entity object is stored in the database;
- It is necessary to check the data in the database during the validation process, for example, to check that there are enough goods in place to accept the order;
- It is necessary to view not only the entity object, it seems
Order
, but also the objects associated with the entity, for example, the objectsOrderItems
for the entityOrder
; - We want to track inserts, updates, or deletes only for some classes of entities, for example, only for entities
Order
andOrderItem
, and we do not need to check for changes in other classes of an entity during a transaction.
Transaction listeners
CUBA transaction listeners also operate in the context of transactions, but, compared to entity listeners, they are invoked for each database transaction.
These gives them super strength:
- nothing can escape their attention.
But their shortcomings determine this:
- they are harder to write;
- they can significantly reduce performance;
- They should be written very carefully: a bug in the transaction listener can even prevent the initial loading of the application.
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.
Take a look at a sample that checks if the entity has an annotation @FraudDetectionFlag
, and, if there is, starts the 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 as quickly as possible.
@Component("passportnumber_ApplicationTransactionListener")
publicclassApplicationTransactionListenerimplementsBeforeCommitTransactionListener{
private Logger log = LoggerFactory.getLogger(ApplicationTransactionListener.class);
@OverridepublicvoidbeforeCommit(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());
thrownew ValidationException(msg);
}
}
}
...
}
ApplicationTransactionListener.java
To turn into a transaction listener, a managed bean must implement the interface BeforeCommitTransactionListener
and method beforeCommit
. 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 storing data through the 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 complicated, 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. In addition, the validation methods listed here do not look so unnatural when they are integrated into a platform that has validators for beans and listeners and that automatically integrates them with the client layer.
In conclusion, we formulate the rules to help choose the best validation method:
- JPA validation has limited functionality, but is a good choice for the simplest constraints in entity classes, if such constraints can be mapped to DDL.
- Bean Validation is a flexible, concise, declarative, reusable and easy-to-read way to customize most checks in domain classes. In most cases, this is the best choice if you do not need to run validations inside transactions.
- Contract validation is bean validation, but for method calls. Use it for method input and output parameters, for example, in REST controllers.
- Entity listeners: while not as declarative as the Bean Validation annotations, they are great for testing large object graphs or for checking inside a database transaction. For example, when you need to read data from the database for a decision. Hibernate has an analogue of such listeners.
- Transaction listeners are a dangerous but powerful weapon that works within the context of a transaction. Use it when in the execution process you need to decide which objects should be checked or when you need to check many different types of entities using the same validation algorithm.
PS: I hope this article has refreshed your knowledge of various validation methods in enterprise applications in Java, and threw out a few ideas on how to optimize the architecture of the projects you are working on.
useful links
Standards and their implementation
- JSR 303 - Bean Validation 1.0
- JSR 349 - Bean Validation 1.1
- JSR 349, Bean Validation Specification
- JSR 380, Bean Validation Specification
- Hibernate Validator 5.4.2 (JSR 349) reference
- Hibernate Validator 6.10 (JSR 380) reference
- Hibernate Validator main page
- Hibernate validator 6.10 API docs
- HV 6.10: Declaring and validating method constraints
- HV 6.10: Cross parameter constraints
Библиотеки
- CUBA bean validation
- Validation cookbook for CUBA applications
- JavaFX with Bean Validation and CDI 2.0
- OVal — non a JSR 303, 349, 380 compliant validation framework
- Spring, Java Bean Validation Basics
- Validation, Data Binding, and Type Conversion in Spring 4.1
Философия валидации данных
- Form validation best practices
- JavaFX Form Validation
- Blah vs Bean Validation: you missed the point like Mars Climate Orbiter
- Avoiding Many If Blocks For Validation Checking