In java tutorials - Annotations part, question 3 (https://docs.oracle.com/javase/tutorial/java/annotations/QandE/questions.html), an annotation are expected to be used as below:
#Meal("breakfast", mainDish="cereal")
I tried to define the annotation as below but it does not allow the above usage.
public #interface Meal {
String value();
String mainDish();
}
Is it possible to omit the first attribute name as the question suggested?
No, the shortcut only works if you specify the value attribute and nothing else.
Otherwise you must explicitly write value=, that is the correct version would be #Meal(value = "breakfast", mainDish = "cereal")
Related
Using Jakarta Been Validation https://hibernate.org/validator/ to validate Java code, I annotate a Classes property by a default #NotEmpty validation anotaton
#Validate
public class A {
..
#NotEmpty
private List<String> myList;
..
}
In a Test I set myList to an empty List, validate that List and expect the violation "must not be empty", which is the default violation message for the standard #NotEmpty annotation. I found that by printing it, but also here https://github.com/hibernate/hibernate-validator/blob/main/engine/src/main/resources/org/hibernate/validator/ValidationMessages.properties
Set<ConstraintViolation<A>> violations = validator.validate(A);
assertThat(violations).anyMatch(having Message("must not be empty", A.class));
This works, but I would like to replace the expected Message string with a reference to the default-string like jakarta.validation.constraints.Null.message, avoiding duplication of Strings. This however does not work:
assertThat(violations).anyMatch(having Message(jakarta.validation.constraints.Null.message, A.class));
Is there any validation expert around, that can show me what I am missing?
one has to load it from the classpath, if provided by /ValidationMessages.properties (or language specific ValidationMessages_<x>(_<y>).properties):
Properties ps = new Properties() ;
ps.load( getClass().getResourceAsStream("/ValidationMessages.properties") ) ;
assertThat(violations).anyMatch(having Message(
ps.getProperty( "jakarta.validation.constraints.Null.message"),
A.class))
if provided/overridden by direct class annotation, one has to use reflection on the specific classes field or method and get its annotation.
I'm already familiar with the base behavior of Spring's #Value annotation to set a field to the value of a project property like so:
Project's Property File
foo.bar=value
Project's Configuration Class
#Configuration
public class MyConfig {
#Value("${foo.bar}")
private String myValue;
}
However I'm trying to make a SpringBoot starter project with conditional configuration and would like to standardize the property names to something useful such as "com.mycompany.propertygroup.propertyname", but to ease transition and encourage adoption, I want to support the old property names for a time as well, and was thus wondering if there was some way to allow multiple property names to set the same field? For instance:
My Theoretical Starter's Config
#Configuration
public class MyConfig {
#Value("${com.mycompany.propertygroup.propertyname}" || "${oldconvention.property}")
private String myValue;
}
Project A's Property
oldconvention.property=value
Project B's Property
com.mycompany.propertygroup.propertyname=value
I can't seem to find any documentation or SO answers on whether or not this is possible and how to achieve it if so... So I'm wondering if it is possible, or if it's not, is there an alternative to the #Value annotation that can be used to achieve the same effect?
Edit to Clarify:
I would not want to keep track of multiple values so I do not need instruction on how to get multiple values... the objective is to consolidate into a SINGLE VALUE that which may have multiple names. In practice, it would only ever have one name-value per project that uses the starter... only in rare cases when someone perhaps forgot to delete the old property would each property name be used (and it would probably have the same value anyway). In such cases, the NEW CONVENTION NAME-VALUE WOULD BE THE ONLY ONE USED.
Update
While the SpEL expression answers provided works when both properties are present, the application context cannot load when only one of the property names is present. Example:
Updated Configuration Class
#Value("#{'${com.mycompany.propertygroup.propertyname}' != null ? '${com.mycompany.propertygroup.propertyname}' : '${oldconvention.propertyname}'}"
private String myProperty;
Updated Property File
com.mycompany.propertygroup.propertyname=somevalue
Error
Caused by: java.lang.IllegalArgumentException:
Could not resolve placeholder 'oldconvention.propertyname' in value
"#{'${com.mycompany.propertygroup.propertyname}' != null ? '${com.mycompany.propertygroup.propertyname}' : '${oldconvention.propertyname}'}"
Requiring both property names to be present defeats the purpose, which is to allow an implementing project to configure this starter using EITHER the old convention OR the new convention...
Another Update...
I've been playing around with the SpEL expression a bit, and I've got the conditional check working when the property is present and when it's not, but I'm having trouble with property resolution after the fact. I think the problem is because property defaults and complex SpEL expressions don't play nice together.
#Value("#{${com.mycompany.propertygroup.propertyname:null} != null ? '${com.mycompany.propertygroup.propertyname}' : '${oldconvention.propertyname}'}")
private String myProperty;
When my SpEL is written like the above, I get a cannot resolve property placeholder exception, meaning that both properties have to be present in order for the SpEL expression to evaluate. So I got to thinking, I could use the default property syntax that I've seen for resolving optional properties: #Value("${myoptionalproperty:defaultValue}")
So below is my attempt to combine the default property resolution with the SpEL expression:
#Value("#{${com.mycompany.propertygroup.propertyname:null} != null ? '${com.mycompany.propertygroup.propertyname:}' : '${oldconvention.propertyname:}'}")
private String myProperty;
When using the default property notation, I keep getting this error:
org.springframework.expression.spel.SpelParseException:
EL1041E: After parsing a valid expression, there is still more data in the expression: 'colon(:)'
and when I Googled that error, the popular answer was that properties had to be wrapped in single quotes so that they evaluate to a string... but they're already wrapped (except the first one.. I had to unwrap that one since I wanted that to evaluate to a literal null for the null check). So I'm thinking that defaults can't be used with properties when they're wrapped in a spell expression. In truth, I've only ever seen the default property set when a #Value annotation is set with just a pure property holder, and all properties I've seen used in a SpEL expression never had a default set.
You can use the following #Value annotation:
#Configuration
public class MyConfig {
#Value("#{'${com.mycompany.propertygroup.propertyname:${oldconvention.propertyname:}}'}")
private String myValue;
}
This #Value annotation uses com.mycompany.propertygroup.propertyname if it is provided and defaults to oldconvention.property if com.mycompany.propertygroup.propertyname is not provided. If neither is provided, the property is set to null. You can set this default to another value by replacing null with another desired value.
For more information, see the following:
Spring Expression Language (SpEL)
Spring Expression Language Guide
As an alternative, you can capture both values and do a selection before returning the value:
#Configuration
public class MyConfig {
#Value("${com.mycompany.propertygroup.propertyname:}")
private String newValue;
#Value("${oldconvention.propertyname:}")
private String oldValue;
public String getValue() {
if (newValue != null && !newValue.isEmpty()) {
// New value is provided
System.out.println("New Value: " + newValue);
return newValue;
}
else {
// Default to the old value
return oldValue;
}
}
}
Using SPEL is the best way to solve this. This should work
#Value("#{'${com.mycompany.propertygroup.propertyname}' != null ? '${com.mycompany.propertygroup.propertyname}' : '${oldconvention.property}'}")
private String myValue;
No that's not possible I believe but yes you can define property as comma separated. For example
com.mycompany.propertygroup.propertyname=value1,value2,value3
And instead of receiving a String you can annotate #Value over String[] like this:
#Value("#{'${com.mycompany.propertygroup.propertyname}'.split(',')}")
private String[] propertyNames;
Another way you can also store key and value as a comma-separated string in the property file and use #Value annotation you can map into Map, For example, you want group name as key and value as group details so in the property file you can store string like this
group.details.property= {'group1':'group1.details','group2':'group2.details'}
And you can annotate #Value as
#Value("#{${group.details.property}}")
private Map<String, String> groupMap;
Trying to figure out how to get along with Java 8 Repeatable annotations support.
Following :
https://blog.idrsolutions.com/2015/03/java-8-repeating-annotation-explained-in-5-minutes/
it works flawlessly.
But if I'm modifying the example and add just a #Manufacturer on the Car class I'm unable to read that single annotation. So if there's only 1 occurrence of the repeated annotation it can't be read.
So having:
#Manufacturer("Range Rover")
public class Car {
}
Manufacturer[] a = Car.class.getAnnotationsByType(Manufacturer.class );
the size here will be 0
and
Cars cars = Car.class.getAnnotation(Cars.class);
for(Manufacturer car: cars.value())
System.out.println(car.value());
here there will be a NPE on the cars..
Why's that ?
You try to get an annotation of type Cars and not of type Manufacturer.
The following solution worked:
Manufacturer[] annotations = Car.class.getAnnotationsByType(Manufacturer.class);
for (Manufacturer annotation : annotations) {
System.out.println(annotation.name());
}
You should always pass the type of the annotation to getAnnotation or getAnnotationsByType and not the type of the class itself.
Hope it helpt you
So the problem is that the Manufacturer annotation doesn't have a RetentionPolicy declared only the container annotation (Cars).
So adding
#Retention(RetentionPolicy.RUNTIME)
also on the Manufacturer annotation will read the annotation in a container based /single based manner (in a manner of speaking)
Thanks Sean, at least you answer gave me a clue...
Is it possible to set properties of annotation programmatically. So if I have :
#interface Author(
String name();
Date date ();
)
I want to be able to set the name property inside the code as if it is a regular class. Is this possible?
You can't use Date in an annotation
To access it at runtime, you need to add the #Retention(RetentionPolicy.RUNTIME) retention policy
The declaration for annotations uses {} for the annotation definition, not ()
No, you can't modify annotations during runtime using normal Java functionality
You may do this using a bytecode manipulator, like Javassist. But that is is not trivial.
Why do you want to do this during runtime? Possibly there are other better solutions to your problem.
In Java, is there a way to change the behaviour of an annotation depending on the type of the annotated field?
I know that annotation presence is supposed to be tested by code. Not the opposite. But the case is rather particular: this is a Jackson 2.0 « inside » annotation which gather a list of annotations. We use it to define the field name (#JsonProperty) and the field serializing policies (#JsonSerialize).
The serialisation policies must be adapted to the annotated field. And, because we are talking of a framework, one unique annotation is far better than two separate ones.
#Retention(RUNTIME)
#JacksonAnnotationsInside.
#JsonProperty("_id")
#JsonSerialize(using=IdSerializer.class)
#JsonDeserialize(using=IdDeserializer.class)
public #interface Id {}
Some cases need to turn the serializers down, that's the point. In the following example, the String must be processed by the de/serializers, ObjectId don't. Both need to be renamed _id by the #JsonProperty.
public class Car {
#Id String id
}
public class Bus {
#Id ObjectId id
}
Any clues?