If you’ve been programming in Java and using any one of the popular frameworks like Spring and Hibernate, you should be very familiar with using annotations. When working with an existing framework, its annotations typically suffice. But, have you ever found a need to create your own annotations?
Not too long ago, I found a reason to create my own annotations for a project that involved verifying common data stored in multiple databases.
The Scenario
The business had multiple databases that were storing the same information and had various means of keeping the data up to date. The business had planned a project to consolidate the data into a master database to alleviate some of the issues involved with having multiple sources of data.
Before the project could begin however, the business needed to know how far out of sync the data was and make any necessary corrections to get in back in sync. The first step required creating a report that showed common data that belonged in multiple databases and validated the values, highlighting any records that didn’t match according to the reconciliation rules defined. Here’s a short summary of the requirements at the time:
- Compare the data between multiple databases for a common piece of data, such as a customer, company, or catalog information.
- By default, the value found should match exactly across all of the databases based upon the type of value.
- For certain fields, we only want to display the value found and not perform any data comparison.
- For certain fields, we only want to compare the value found and perform data verification on the specific data sources specified.
- For certain fields, we may want to do some complicated data comparisons that may be based on the value of other fields within the record.
- For certain fields, we may want to format the data in a specific format, such as
$000,000.00
for monetary amounts. - The report should be in Microsoft Excel format, each row containing the field value from each source. Any row that doesn’t match according to the data verification rules should be highlighted in yellow.
Annotations
After going over the requirements and knocking around a few ideas, I decided to use annotations to drive the configuration for the data comparison and reporting process. We needed something that was somewhat simple, yet highly flexible and extensible. These annotations will be at the field level and I like the fact that the configuration won’t be hidden away in a file somewhere on the classpath. Instead, you’ll be able to look at the annotation associated with a field to know exactly how it will be processed.
In the simplest terms, an annotation is nothing more than a marker, metadata that provides information but has no direct effect on the operation of the code itself. If you’ve been doing Java programming for a while now, you should be pretty familiar with their use, but maybe you’ve never had a need to create your own. To do that, you’ll need to create a new type that uses the Java type @interface
that will contain the elements that specify the details of the metadata.
Here’s an example from the project:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReconField {
boolean compareSources() default true;
ReconDisplayFormat displayFormat() default ReconDisplayFormat.NATIVE;
String id();
String label() default "";
ReconSource[] sourcesToCompare() default {};
}
This is the main annotation that will drive how the data comparison process will work. It contains the basic elements required to fulfill most of the requirements for comparing the data amongst the different data sources. The @ReconField
should handle most of what we need except for the requirement of more complex data comparison, which we’ll go over a little bit later. Most of these elements are explained by the comments associated with each one in the code listing, however there are a couple of key annotations on our @ReconField
that need to be pointed out.
@Target
– This annotation allows you to specify which Java elements your annotation should apply to. The possible target types are ANNOTATION_TYPE
, CONSTRUCTOR
, FIELD
, LOCAL_VARIABLE
, METHOD
, PACKAGE
, PARAMETER
and TYPE
. In our @ReconField
annotation, it is specific to the FIELD
level. @Retention
– This allows you to specify when the annotation will be available. The possible values are CLASS
, RUNTIME
and SOURCE
. Since we’ll be processing this annotation at RUNTIME
, that’s what this needs to be set to.
This data verification process will run one query for each database and then map the results to a common data bean that represents all of the fields for that particular type of business record. The annotations on each field of this mapped data bean tell the processor how to perform the data comparison for that particular field and its value found on each database. So let’s look at a few examples of how these annotations would be used for various data comparison configurations.
To verify that the value exists and matches exactly in each data source, you would only need to provide the field ID and the label that should be displayed for the field on the report.
@ReconField(id = CUSTOMER_ID, label = "Customer ID")
private String customerId;
To display the values found in each data source, but not do any data comparisons, you would need to specify the element compareSources
and set its value to false
.
@ReconField(id = NAME, label = "NAME", compareSources = false)
private String name;
To verify the values found in specific data sources but not all of them, you would use the element sourcesToCompare
. Using this would display all of the values found, but only perform any data comparisons on the data sources listed in the element. This handles the case in which some data is not stored in every data source. ReconSource
is an enum
that contains the data sources available for comparison.
@ReconField(id = PRIVATE_PLACEMENT_FLAG, label = "PRIVATE PLACEMENT FLAG",
sourcesToCompare ={ ReconSource.LEGACY, ReconSource.PACE })
private String privatePlacementFlag;
Now that we’ve covered our basic requirements, we need to address the ability to run complex data comparisons that are specific to the field in question. To do that, we’ll create a second annotation that will drive the processing of custom rules.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReconCustomRule {
String[] params() default {};
Class<?> processor() default DefaultReconRule.class;
}
Very similar to the previous annotation, the biggest difference in the @ReconCustomRule
annotation is that we are specifying a class that will execute the data comparison when the recon process executes. You can only define the class that will be used, so your processor will have to instantiate and initialize any class that you specify. The class that is specified in this annotation will need to implement a custom rule interface, which will be used by the rule processor to execute the rule.
Now let’s take a look at a couple of examples of this annotation.
In this example, we’re using a custom rule that will check to see if the stock exchange is not the United States and skip the data comparison if that’s the case. To do this, the rule will need to check the exchange country field on the same record.
@ReconField(id = STREET_CUSIP, label = "STREET CUSIP", compareSources = false)
@ReconCustomRule(processor = SkipNonUSExchangeComparisonRule.class)
private String streetCusip;
Here’s an example where we are specifying a parameter for the custom rule, in this case, it’s a tolerance amount. For this specific data comparison, the values being compared cannot be off by more than 1,000. By using a parameter to specify the tolerance amount, this allows us to use the same custom rule on multiple fields with different tolerance amounts. The only drawback is that these parameters are static
and can’t be dynamic due to the nature of annotations.
@ReconField(id = USD_MKT_CAP, label = "MARKET CAP USD",
displayFormat = ReconDisplayFormat.NUMERIC_WHOLE, sourcesToCompare =
{ ReconSource.LEGACY, ReconSource.PACE, ReconSource.BOB_PRCM })
@ReconCustomRule(processor = ToleranceAmountRule.class, params = { "10000" })
private BigDecimal usdMktCap;
As you can see, we’ve designed quite of bit of flexibility into a data verification report for multiple databases by just using a couple of fairly simple annotations. For this particular case, the annotations are driving the data comparison processing so we’re actually evaluating the annotations that we find on the mapped data bean and using those to direct the processing.
Conclusion
There are numerous articles out there already about Java annotations, what they do, and the rules for using them. I wanted this article to focus more on an example of why you might want to consider using them and see the benefit directly.
Keep in mind that this is only the starting point, once you have decided on creating annotations, you’ll still need to figure out how to process them to really take full advantage of them. In part two, I’ll show you how to process these annotations using Java reflection. Until then, here are a couple of good resources to learn more about Java annotations:
– Jonny Hackett, asktheteam@keyholesoftware.com