Spring Framework provides a ready-to-use, simple validation framework out of the box. In this article, I'll explore how to use Java annotations to make Spring's validation framework even more convenient to use, while providing a basic introduction to Spring validation and Java annotations in general.
Introduction
Spring's data binding and validation framework is simple but functional - all you need to do is implement the Validator
interface. In particular, if you use Spring's MVC framework (and its command/form controllers) then you're probably using Spring validation as well. If not, you should be! Moving your validation outside the controller into a Validator
implementation allows you to declaratively assign and reuse your validation logic.
But while you can declaratively assign and reuse Validator
s with your form or command controllers, doing so only allows you to declare validation on a 'per-class' level, with no field-level granularity.
A typical scenario
For example, consider the typical scenario where you have a LoginForm
class to represent your typical login form with two fields username
and password
. It's trivial to write a LoginFormValidator
that checks that both fields are present and assign that to the controller that handles login.
What if you now need a RegistrationForm
controller, with additional fields such as verifyPassword
and emailAddress
? Well, you could extend LoginFormValidator
or simply add an new Validator
to the registration controller for the additional required fields. All is still good and dandy.
But what about other form or command objects? What if, a PasswordChangeForm
needs only the password
and verifyPassword
fields? How about a UserSearchForm
with only the username
?
You could of course write unique Validator
s for individual fields, but that's a terrible CodeSmell. At this point you know you need to write a more generic validation implementation with field-level granularity.
But, depending upon your approach, you might end up not being able to use Spring's declarative approach to assigning validators to controllers. Or, you might have to implement your own mechanism to allow declarative validation rules using fancy XML. Or use YetAnotherThirdPartyLibrary, which, for a small application or during its initial stages may be overkill and cause unnecessary code bloat.
Annotations to the rescue
Fortunately, Java 5.0 annotations give us a convenient way to mark object elements or fields in a way that lets us roll out an equally simple and extensible, yet more granular validation implementation based on Spring.
The rest of this article details a simplistic approach to this, using the common, if rather trivial example above. At the end of the article I have some thoughts on how one can take this approach and make it more functional and useful.
Our typical LoginForm
package foo.bar;
public class LoginForm {
private String username;
private String password;
public String getUsername() {
return this.username;
};
// snip...
}
Nothing exciting happening here, I'll just put it here for reference.
Annotating classes
First we'll want to 'mark' our class so that our annotation-aware validators can easily recognize them later (without having to inspect down to the class members). Let's create our first annotation:
package foo.bar;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Validatable {
}
As you can see, there's really nothing there. It's a plain marker annotation. A few things about the annotations that annotate our annotation are in order, though.
The "@Target(ElementType.TYPE)
" annotation tells the compiler that Validatable
should only be applied to Types or classes.
"@Retention(RetentionPolicy.RUNTIME)
" indicates that this annotation should be recorded into the class file and retained by the VM at run-time, enabling it to be introspected. This is probably the most important part of this whole annotation business for our purposes, as it's crucial to how our validator will work later.
We can now use @Validatable
to annotate our classes so they can be identified and validated by our validation framework later. Next, we'll create the @Required
annotation to indicate required fields.
Annotating fields
package foo.bar;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Required {
}
Note that while I said @Required
will be used to annotate required fields, I indicate that it's target should be a METHOD
. In particular, we'll be using @Required
to annotate field or property 'getters'.
There are two reasons for this. One is I'm a lazy bum who likes to get things done the simplest way possible. Next is that if we use @Required
on a class' private fields then we'd have to do double work to introspect the class just to find our annotation(s) (as opposed to being able to use Spring's BeanUtils
to help us out). So instead, I opt to use @Required
on the getField() methods.
Annotations in action
Armed with these two annotations, let's go ahead and apply them to our LoginForm
.
@Validatable
public class LoginForm {
// ...snip
@Required
public String getUsername() {
return this.username;
};
// ...snip...
@Required
public String getUsername() {
return this.username;
};
// snip...
}
As you can see, writing annotations is quite painless, especially simple marker annotations with no parameters (or elements) like ours.
Also, using annotations in one's code doesn't require much work, either (just remember to import
them if they're in a different package). In fact, annotations can lend to better code clarity - after all, that's what they're there for in the first place.
A Spring Validator in 3 easy steps
Now let's make this magic happen. We'll write a RequiredAnnotationValidator
that uses introspection to find our annotations, and perform validation on the required fields.
Step 1: Implement Validator
public class RequiredAnnotationValidator implements Validator {
// snip...
Because we're implementing Spring's Validator
interface, we need to implement its two primary methods supports()
and validate()
.
Step 2: Implement supports()
What classes should our RequiredAnnotationValidator
support? Why, classes annotated as @Validatable
, of course. How do we find out if a class is annotated as @Validatable
using introspection? Why, it's almost trivial:
@SuppressWarnings("unchecked")
public final boolean supports(final Class clazz) {
return clazz.isAnnotationPresent(Validatable.class);
}
As you can see if we had only used our @Required
annotation, our validator would have to do a lot more work - go through the methods of the given Class clazz
sequentially, looking for any methods annotated as @Required
and returning true
once it finds one. Annotating the class itself is simpler and faster.
A little note about the @SuppressWarnings
above. If you're using Eclipse IDE, the line "clazz.isAnnotationPresent(...)
" will generate a warning because we're calling a method on the raw type Class
(and not a parameterized version of the generic type Class < T >
). It's kind of a nuisance - though we can get rid of the warning with a fugly cast: "Class<?> c = clazz
" - so we tell Eclipse to just shut up.
Step 3: Implement validate()
Now to the meat of the matter, implementing validate()
. Now that you've seen how supports()
works, you should have a general idea of how validate()
is supposed to work.
First we'll want a list of getter methods. We could use standard Java Reflection, or, since we're already using Spring:
public final void validate(final Object obj, final Errors errors) {
final PropertyDescriptor[] propertyDescriptors
= BeanUtils.getPropertyDescriptors(obj.getClass());
...
We can rely on BeanUtils.getPropertyDescriptors()
for convenience since, by our own convention, we're looking for annotations in property getter methods. As I mentioned earlier, had we annotated private fields, we'd have to use Class.getDeclaredFields()
, iterate through the fields looking for corresponding accessor methods, etc. Our approach is a lot less painful.
What do we do with each property descriptor? First check if the property has a read method, and if that method is annotated as @Required
(since we're using Java 5.0, we'll go ahead and use an enhanced for
loop):
for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {
final Method method = propertyDescriptor.getReadMethod();
if (method != null && method.isAnnotationPresent(Required.class)) {
At this point we know that the field foo
is required because the method getFoo()
was annotated with @Required
. Now all we have to do is check that its value is not null
, or, since we want to be a little smart, check that it's not the empty string either (""
). Fortunately, once again Spring provides utility code in ValidationUtils
to do just the job:
final String field = propertyDescriptor.getName();
ValidationUtils.rejectIfEmpty(errors, field, field + ".required");
Here we just told Spring to check if the given field is empty, and if it is, reject it with the error code "fieldname.required" and place that error into the passed in errors
object.
The complete validator
Here's a more-or-less complete look at our RequiredAnnotationValidator
(minus any import
statements):
package foo.bar;
public class RequiredAnnotationValidator implements Validator {
@SuppressWarnings("unchecked")
public final boolean supports(final Class clazz) {
return clazz.isAnnotationPresent(Validatable.class);
}
public final void validate(final Object obj, final Errors errors) {
final PropertyDescriptor[] propertyDescriptors
= BeanUtils.getPropertyDescriptors(obj.getClass());
for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {
final Method method = propertyDescriptor.getReadMethod();
if (method != null && method.isAnnotationPresent(Required.class)) {
final String field = propertyDescriptor.getName();
ValidationUtils.rejectIfEmpty(errors, field, field + ".required");
}
}
}
}
Voila!
Annotation-based validation based on Spring's validation framework in a couple of dozen lines of code, how's that? Here's how we'd use it in an sample Spring application context XML descriptor file:
<bean name="requiredAnnotationValidator" class="foo.bar.RequiredAnnotationValidator"/>
<!-- e.g., foo.bar.LoginController extends SimpleFormController -->
<bean name="loginController" class="foo.bar.LoginController">
<property name="commandClass"><value>foo.bar.LoginForm</value></property>
<property name="validator"><ref bean="requiredAnnotationValidator"/></property>
</bean>
If we need to use more than one validator for LoginController, then we simply use the plural validators
property of BaseCommandController
.
Conclusion
Simple, extensible and granular declarative validation is possible using the Spring framework, aided by Java annotations.
There are a lot of more things we can do from here. We could use an annotation element as a parameter to declaratively specify the error code for our required field (as opposed to a hard-coded "field.required"). We can go ahead and create other annotations, for example, @MaxLength
with a length parameter.
One drawback to our approach is the fact that the validation has to perform introspection every time it's asked to validate an object. For form command object binding and validation, this isn't such a problem. However, this can lead to performance issues especially if the validation is called repeatedly on a large collection of objects.
One enhancement I could think of is to have the validators cache the introspection results so they don't have to do it every time (and do so in a thread-safe manner).
This article has outlined a gentle introduction Spring validation as well as Java annotations. I hope now you know how easy they both can be, and that you're now as excited as I am on their added posssiblities.
Alistair Israel started learning programming with BASIC on a TRS-80. That was a long time ago. He's still learning.
分享到:
相关推荐
主要介绍了Spring Validation实现原理分析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
2020年最新Spring Boot Validation讲透讲全,别找其他的了。包括了平时用到的所有用法以及代码实例。
Annotation-Based Validation with the Spring Bean Validation Framework
Deep Learning: Practical Neural Networks with Java by Yusuke Sugomori English | 8 Jun. 2017 | ASIN: B071GC77N9 | 1057 Pages | AZW3 | 20.28 MB Build and run intelligent applications by leveraging key ...
This book is meant for Java developers with little or no knowledge of Spring Framework. All the examples shown in this book use Spring 4. You can download the examples (consisting of 60 sample ...
spring注解-validation所用到的jar包
spring-modules-validation-0.6.jar
HTML Form Validation with Javascript
A3 Diagnostic Validation with CANoe.DiVa
validation spring2.5 + spring-modules-validation 扩展 满足业务上复杂的判断逻辑,包括错误码动态设定,和业务逻辑判断
非常不错的数据校验jar,与spring的无缝接入,是java pojo对象校验的好框架。
常用jar包
这是java和xml联合编程可使用的文档!
resin 支持spring mvc 5.0以上版本 支持Hibernate validation 下载使用即可版本为 resin-4.0.61
Building RESTful Web Services with Java EE 8 is a comprehensive guide that will show you how to develop state-of-the-art RESTful web services with the latest Java EE 8 APIs. You will begin with an ...
标签:jakarta、validation、api、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译,请...
主要介绍了如何使用Spring Validation优雅地校验参数,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
Spring验证 我正在使用该项目作为有关如何改进传入数据的Spring验证的演示/参考项目。 这个想法是要帮助我摆脱原始痴迷,并改善如何验证数据的方式。 进行改进 确保所有内容都有GET / POST / PUT / DELETE调用。 ...
java运行依赖jar包
标签:javax、api、validation、jar包、java、API文档、中文版; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准...