Currently there is no standard way to add validity constraints to Java classes. This is in contrast to other languages like XML in which (using XML Schema) relatively rich constraints may be specified. With the advent of annotations (JSR 175) in the Java 2 Platform, Standard Edition (J2SE) 5.0 a convenient mechanism now exists to allow such constraints to be specified in the Java meta model. This article explores how annotations might be used for this purpose and discusses why this may be an important capability. This article is made up of 3 parts:
In this section we'll take a look at what Java has to offer in terms of support for constraints and compare this with other technologies that a Java 2 Platform, Enterprise Edition (J2EE) developer would typically encounter. First, lets start with some definitions to make sure we are all on the same page. What we mean by the term constraint (for the purposes of this article) is simple field level constraints on data. For example, to store a person's age we may define an integer field called age. In plain English, a constraint on this field would be something like "the age must be greater than or equal to 0". When we define an entity, such as a person, in a language like Java then we do so in the meta model of that language. In the case of Java, the meta model that we use is for this is a Class. So if we define an age field as "private int age;" then we are specifying three pieces of meta information: the visibility; the type and the name. The meta model for Java is defined in the Java Language Specification. Why We Care About Constraints
In most computer systems, the integrity of the data is paramount. For example, the data may represent a bank account or patient record. The usefulness of that information, to the organization that maintains it, is typically related to the accuracy and integrity of that information. For example, if a bank account does not have a valid account number then it is difficult for the bank to make proper use of that account. In an n-tier system, the business tier exposes services that provide an access point to parts of a business process that is being automated. One of the key responsibilities of these business services is to ensure that the integrity of the business data is maintained. They do this by enforcing certain business rules on that data during the life cycle of that data. Additionally, databases are normally designed with a certain degree of integrity checking built in. Unfortunately, if no integrity checking is performed on the data prior to it reaching these business services, then the user experience may suffer because of the time it takes between the user submitting the information and the user being informed of errors in the data. Additionally, increased load is placed on the system as it needs to cope with a higher amount of messages to the business services for each business transaction. To alleviate these problems, it is common to do some integrity checks on the data closer to the end user. Some checks may be done in the browser using the facilities in HTML forms or by using JavaScript. Checks are also typically done in the presentation tier which is the front door to the server side processing. These checks are a subset of the overall business rules as it is typically impractical to attempt to validate the data completely. A typical example would be for a credit card number. You might check that the card number adheres to the correct format in the browser or presentation tier, but ensuring that it is a valid credit card number and has sufficient credit will typically be left to the business tier. In trying to solve this problem, however, new problems are often introduced. One problem is that the business rules are duplicated. For example if you have a rule specifying the constraints on a credit card number and you hand code this check in the browser, presentation tier and business tier, then you have duplicated that rule three times. If the rule changes then you need to change this in each place. This is clearly a maintenance issue. Another problem is missing rules in the business tier, as a result of adding those rules in the browser or presentation tiers. This may occur deliberately to avoid maintenance problems associated with duplication, or accidentally because the data reaching the business services no longer has these integrity issues. At first this may not seem like a problem. However, it means that the business services are no longer in charge of maintaining the integrity of that data, but are instead trusting their clients to look after some aspects of that integrity. The moment a business service is reused, then the new clients must know that they need to do the same validity checks or you now risk the integrity of your businesses data. In an SOA world, where we want to increase the reuse of business services, we cannot afford to have the business services not taking complete responsibility for the validity of the data. This is particularly important for business services that are used between separate organizations such as business partners. Clearly, you can't rely on your business partner to ensure the validity of your businesses data. One approach to solving this issue (at least partially), is to declaratively define a set of constraints on the data, that can be shared by various producers and consumers of that data. This approach is adopted by the Web Services standards which use XML Schema to define data exchanged in these services. This is also the approach adopted within this article, where we introduce a mechanism to define constraints using the new Annotation facility in J2SE 5.0. But first, we dig a little deeper into constraints. Comparing Constraint Facilities in Some Meta Models
Consider Figure 1 below. It shows a simple application which captures address information from the user in a web browser, transfers it to a J2EE application server which in turn both stores the address in a local database as well as passes the address to a partner's web service. In addition, there is a simple business rule (constraint) associated with the address specifying that the street field cannot be greater than 20 characters in length.
At each node in the application, the address data is captured in a different format and governed by a different meta model. Taking the street field, we can see that in the browser it is in the form of an HTML text input field; in the application server it is in the form of a String property of an Address Java class; in the database it is a VARCHAR column of an ADDRESS table; and in the partner's web service it is a string element within an address element of an XML document. Lets take a closer look at the meta model that applies to the address data at each of these nodes. In The Browser Here we represent the street field as an input text field. In HTML 4.0 the meta model of an input field allows for some constraints to be placed on the data to be captured. In particular it allows us to specify a maxlength attribute (as shown in the diagram) which will cause browsers to prevent the users from typing more than that many characters into the field. The meta model allows for the definition of this constraint and the browser will enforce the constraint. Beyond maxlength there are some other constraints, such as option lists, which restrict the user to choose one of a fixed set of options, but on the whole the list of constraints is fairly limited. As a result, web developers typically resort to the use of JavaScript to enforce other constraints in the browser. The next generation of forms known as XForms leverages the XML Schema meta model and contains a far richer set of constraints. In The Database In the database we represent the street field as a VARCHAR column of an ADDRESS table. The relational database meta model allows us to specify a maximum length constraint on VARCHAR columns as is shown in the diagram. So once again, the meta model allows for the definition of the constraint and the RDBMS will enforce the constraint. The constraint mechanisms of modern DB's are pretty rich these days. In The Web Service Web Services leverage XML Schema as the meta model for the input and output data of WSDL operations. XML schema allows a relatively rich set of constraints to be specified. For example, our business rule of a maximum of 20 characters on the street field is no problem in XML schema.
The list of XML Schema constraints (known as simple type facets) are: In The Application Server In the application server the street field is represented as a String property of a Java class. The Java meta model does not have a standard mechanism to specify any constraints on a String field. So we cannot directly capture the business rule that restricts the street field to 20 characters, as part of the definition of the street field. Typically such constraints end up being enforced in code. Others have discussed this issue before and proposed some potential solutions. We will cover some of these later in the article. As you can see there is a fair difference in these four meta models when it comes to defining constraints. In the next section we discuss why we think that being able to specify constraints declaratively is a useful thing. Why Meta Model Constraints are Useful
The problem is not that you can't enforce such constraints in the Java programming language. Clearly you can do so programatically as the following example shows.
But how does this differ from, say, the XML example (reproduced below) where the constraint was captured (declaratively) in the meta model?
First thing you might notice is that the enforcement is quite ad hoc. We could have written the Java code in different ways. In fact, it would be quite easy to have forgotten to do a null check first. The second thing is that (ignoring the verbosity of XML Schema) there is more code required to procedurally capture the constraint. Another difference is that there is no separation of the definition of the constraint, from the enforcement of it. In fact, the Java version does not really define the constraint at all, except possibly informally in the JavaDoc. The XML version, in contrast, does not contain any enforcement code. The enforcement would be done in some other language like Java. However, a very important difference is that this code is likely to be part of a framework, rather than an application. Very little, if any, application code would be required for enforcement. And the last difference is that there is no way to programatically determine what constraints are present on the street field. For example, we can't introspect on the field and find the constraints. By separating the definition from the enforcement, and by providing a way to introspect these constraints, we gain much more flexibility in the enforcement. This is particularly useful when integrating with other frameworks and also for sharing with other meta models (which we will cover in the next section). It also allows us to potentially employ different enforcement strategies over time or in different circumstances. Sharing Constraints Between Meta Models
Not only would it be nice to have a standard mechanism for specifying and enforcing constraints within the Java programming language, it would also be useful to be able to share constraints between Java and other meta models. In Figure 1, we showed an example where the Address data moved between 4 different meta models. The diagram showed how the maximum length constraint on the street field could be defined in the various meta models (except Java). Ideally we would like to define this constraint only once and then share it between each of these meta models. For example we would like to be able to do the following sorts of things.
To be able to do all of the above, we need to be able to programatically access to the constraints. Current State of Affairs for Constraint Checking in Java
Most applications today do constraint checking in a fairly informal and ad hoc manner. It is not uncommon to see constraint checking coded manually in the HTML forms, web applications, or EJB's, with little or no attempt to avoid code duplication. It is also not uncommon to find constraint checking missing from some of these places or indeed be inconsistent between them. There are likely many cases where the business rules changed but not all the checking code was changed, leading to unexpected behaviour. Others have discussed this issue before and presented some solutions. The following is a summary of the different approaches we have come across. The resources section contains links to articles that discuss these approaches. Note these articles are worth reading in their own right.
This is by no means an exhaustive list of possible approaches to solving this problem. Many other approaches are possible including having separate classes for each constrained property (having a Street class for the street property instead of String). What we have covered are some of the more popular and successful approaches that we are aware of. Key Features of An Ideal Solution
We have provided an overview of some of the existing approaches and pointed out where we believe there is room for improvement. In wrapping up the first part of this article, let us outline what we see as some of the more important features of an ideal solution. We believe an ideal solution would have the following properties.
In the remainder of this article we will discuss the use of J2SE 5.0 annotations for defining constraints. We believe this fulfils the first four goals and that it would be a good basis for a standard. The remaining goals are beyond the scope of this article. Let's explore a possible way to add constraints by using annotations. If you are not familiar with annotations, then we recommend you read some other articles before you continue.
The code below shows a snippet of an Address class with a
You may have noticed that the street field is declared public. I will deal with this later in the article, but let's ignore this for now. We can define the MaxLength annotation as follows.
Note the annotation is defined by the use of the
The following code snippet shows how you can use reflection to get the
Now that you have the constraint you can validate values against it. The following code illustrates this.
It's straightforward to add constraints to fields and validate them by using annotations and the runtime reflection capabilities associated with them. However, this example relied on the field being public, or else the introspection would have failed. This short coming will be addressed next. JavaBeans Version
So let's adapt this approach to a JavaBeans model, and while we are at it, get a bit more sophisticated in the processing. In this case the field will be private and there will be associated public getters and setters. As mentioned, annotating the field is not useful as we can't sensibly introspect it when it's private. Fortunately there are other places we can put the annotations where we can introspect them. The choices are as follows:
Any of these are possible, and in fact all could be allowed, but we will concentrate on the setter method itself. To be easy for frameworks to identify constraints, it would be useful to have something to mark them out from other annotations. There is no inheritance for annotations, but luckily there is another mechanism -- annotations for annotations. Below is a simple marker meta-annotation that will be applied to our constraint annotations (like
The
Note the
Now we are ready to define a JavaBeans version of Address with an additional
Let's now start to put together some code to get at these property-based constraints. First, we write a simple method to determine if a given annotation is a constraint.
Next we build on that to go through an array of annotations and return from that a set of constraints (Annotations that pass the
Finally we write a method to give us the set of constraints associated with a given property of a given class. Exception handling has been omitted for clarity.
So we now have a simple way to annotate properties on JavaBeans and a simple method to get at them later. To be perfectly honest, we've taken a few liberties with the above code in order to make it simpler to follow. In reality, you wouldn't want to create a new set of constraints from scratch each time. You'd really want to do it once and cache it. Also, the above assumes that the setter methods are not being overridden. In the real world, you'd want to cater for the setter method being inherited from a parent class and also having the setter method being overridden, one or more times, in the inheritance hierarchy. What we want, is the union of all the constraints on the setter method, such that all the constraints on the property, regardless of where they are defined, must be satisfied. This means walking up the inheritance hierarchy, looking for all the definitions of the setter method and collecting the corresponding constraints. You can download the complete code from the resources section. More Constraints
Clearly minimum and maximum lengths are just two examples of constraints that could be added using Annotations. We could define annotations for many of the XML Schema constraints like Validation
Now we have an extensible mechanism for defining constraints on JavaBeans properties and a straightforward way to get at them at runtime. What we need next is a validation framework that allows us to check whether a given value is valid for a given property of a given class. The following code snippet gives you the general idea.
Note that this simple validation method is usable in several different ways. For example, you could call this inside the setter method if you wanted to prevent invalid values being set as shown in the following code snippet. The Validator is some instance of a class that implements the validate method shown above.
Or if you prefer you could allow the value to be set and then validate the field afterwards.
Still another option is to validate a value externally from the bean and only call the setter if it is valid. The last option may be useful when integrating with a presentation framework like JSF.
We won't go into more details on validation for now. Hopefully we've shown enough to demonstrate that you could implement a sophisticated validation framework around these constraints and that you could even integrate it in to some existing validation framework. In this way, you could get a seamless framework that handled simple field constraints, as well as more complex cross field validations and so on. A validation framework is included with the downloadable source. IDE Support
One of the advantages of using annotations is that J2SE 5.0 aware IDEs may provide auto-completion for annotations. This makes it very easy for developers to see the list of constraints that can be applied. The following screenshot shows this feature in NetBeans 4.
It's also worth pointing out that this works best when we have each annotation applying a single constraint and the name of that annotation being fairly self explanatory. For example, it wouldn't be hard to figure out what adding a MaxLength annotation might do, once you were familiar with applying constraints this way. Aligning these annotation names with the names used for corresponding constraints in XML Schema, further simplifies things for developers familiar with XML Schema. The end result is a strong development story around applying property constraints, without the need for custom IDE support (i.e. it's out of the box). Part 3: Further Discussion
In this section we discuss some other possibilities as well as some limitations of this implementation. Constraints as Part of a Property Annotation
We have covered the use of annotations for constraints in isolation from other annotations. The reality is that as developers move to J2SE 5.0, annotations will be created for many things. One area that seems likely, is a set of annotations for JavaBeans properties as a whole. In this case the property-based constraint annotations, as discussed in this article, would need to work together with these annotations or may even be folded into this set. As a simplistic illustration, you might have something like this.
Note the @Property annotation is a slightly different type of annotation from constraint annotations, as it would need to be processed at build time in order to generate the getter and setter methods. Build Time vs. Run Time
We based our approach on the use of the runtime support for annotations, because we wanted the constraints to become part of the class's meta data, in a similar way that constraints within XML Schema are part of the meta data for XML based data. Annotations, however, may also be accessed at build time. Included with the J2SE 5.0 release is a tool known as the Annotation Processing Tool (APT), which is designed to support build time processing of annotations. It allows us to generate additional code, such as additional classes, based on annotations within existing classes. This may be particularly useful, if you want to ensure that property validation is always done in the setter method. To illustrate this, let's revisit our Address class, which is reproduced below.
Using APT we could generate a decorator class (applying the GOF decorator pattern) to enforce validation in the setter method as shown below.
Having validation in the setter method is simply a matter of constructing and using the decorator as shown below.
Boolean Limitations
The approach we have outlined mirrors the approach taken by XML Schema, in that we have explicitly defined a set of constraint objects that can be applied to a property. For example, to constrain the street property to a maximum length of twenty characters, we declaratively add a
When there is more than one constraint applied to a property, all those constraints must be adhered to for the property to be valid. In other words, there is an implicit AND operation between the constraints. For example, if we define a minimum length constraint and a maximum length constraint on the street property as follows:
then enforcement of these two constraints is logically equivalent to enforcement of the following boolean expression:
Note, XML Schema also has an implicit ANDing of constraints, as do frameworks like JSF. There is no support for the use of an OR operator between constraints, nor the use of a NOT operator, nor grouping operators to control operator precedence. In other words, there is only partial support for boolean expression. We consider alignment with XML Schema more important than more complete boolean expression support. We also feel, as the designers of XML Schema and JSF presumably do, that this approach results in a good compromise between power and complexity. The end result is that, while it may not cover every situation, it is easy to use and covers many common cases. Java-Style Boolean Expression Approach
An alternative approach is to have a single annotation to which we pass a boolean expression. For example, the
As we showed in the previous section, this boolean expression is logically equivalent to adding both a PROS
CONS
Incidentally, if we go down this path and add full boolean expression support, we are getting pretty close to what Design By Contract (DBC) implementations like iContract offer. The goal of the constraint mechanism is not to create another DBC solution, but instead to add a simple mechanism for declaratively adding some constraints to properties, which is aligned with important standards like XML Schema and JSF. This is intended to be complimentary to the J2SE 1.4 assertion facility, which provides support for some of the DBC concepts. So while there are some limitations to the approach we have presented in this article, we believe that it provides a pragmatic and useful solution to an important problem. Summary
In this article we have discussed the value of having a mechanism in the Java programming language to be able to declaratively add constraints to properties, and covered some of the existing approaches to solving this problem. Additionally, we have listed what we see as some of the more important features of an ideal solution, and presented a new approach, that leverages the power of J2SE 5.0 annotations to, in our opinion, take us a step closer to that ideal. Resources
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Oracle is reviewing the Sun product roadmap and will provide guidance to customers in accordance with Oracle's standard product communication policies. Any resulting features and timing of release of such features as determined by Oracle's review of roadmaps, are at the sole discretion of Oracle. All product roadmap information, whether communicated by Sun Microsystems or by Oracle, does not represent a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. It is intended for information purposes only, and may not be incorporated into any contract.
|
| ||||||||||||