Java reflection querying annotations by type - java

I'm trying to query the annotations from a class using this code:
for (final Annotation annotation : annotations) System.out.println(annotation);
final JsonSchema[] schemas = clazz.getAnnotationsByType(JsonSchema.class);
if (schemas.length == 0) {
throw new IllegalArgumentException("No JsonSchema annotation found.");
}
If I arrive here via a unit test I get past the schemas.length test. If I do so via a Maven plugin which passes the URL of a class into URLClassLoader().loadClass() then it throws IllegalArgumentException. However, it also prints out:
#mypackage.JsonSchema()
i.e. The annotation appears to be there if I loop through the results of getAnnotations() but isn't found by getAnnotationsByType. What could be causing this?
Edit: If I try looping through and comparing the canonical names then casting to JsonSchema it won't let me as it appears to be a com.sun.proxy which is not an instanceof JsonSchema.
Edit: It's because they're in different class loaders, I'm sure. Quite how to fix that...

Got it.
I was passing an array of URLs for classes to load to new URLClassLoader(). By adding a second parameter of Thread.currentThread().getContextClassLoader() to the constructor it seems to load them into the same ClassLoader and everything then works as expected.

Related

Using Lombok's SuperBuilder with Hibernate Validator (jakarta.validation.x) annotation on a container type leads to "Type mismatch"

I have a class ActivitiesModel which uses Lombok's SuperBuilder.
import jakarta.validation.NotBlank;
// other imports and statements omitted for brevity.
#Data
#SuperBuilder
#NoArgsConstructor
public class ActivitiesModel {
public static final String ACTIVITIES_NOT_NULL_MESSAGE = "Activities cannot be null";
public static final String ACTIVITY_NOT_BLANK_MESSAGE = "Activity cannot be blank";
#NotNull(message = ACTIVITIES_NOT_NULL_MESSAGE)
private List<#NotBlank(message = ACTIVITY_NOT_BLANK_MESSAGE) String> activities;
}
I am using this builder to create an object of ActivitiesModel, and then validating it using Hibernate's Validator interface:
// Somewhere else in the application.
// Create an object using the builder method.
ActivitiesModel activitiesModel = ActivitiesModel.builder()
.activities(List.of("hello", "world")) // <----- Point A
.build();
// Validate the object using Hibernate's validator.
validator.validate(activitiesModel);
However, running this code gives me the following error:
java.lang.Error:
Unresolved compilation problem:
Type mismatch: cannot convert from List<String> to List<E>
The stack trace seems to be pointing at Point A.
I have tried the following approaches:
Replacing the #SuperBuilder with #Builder and #AllArgsConstructor.
Replacing the message attribute with a string literal instead of a static final variable, i.e:
private List<#NotBlank(message = "Activity cannot be blank") String> activities;
1st approach seems to fix this error, however, it's not something I can use as I need to extend the builder functionality to a subclass of ActivitiesModel. Also, this issue is also present in another abstract class, so the super builder functionality for parent classes is definitely required.
2nd approach also works in solving the error. However, going with it is a bit problematic because I then need to have the same message string in the validation test for this model class, which is something I would like to avoid as it duplicates the string.
Another thing to note is that this error only seems to occur in the presence of an annotation on the generic type parameter of the container, which is NotBlank in this case. It is not influenced by any annotations which are present directly on the field itself (NotNull in this case).
So, all in all, these are the questions that I would like to get some answers to:
Somehow, Lombok is able to figure out the types in case of a string literal but not in case of a static final String. Why is that?
Am I going about this totally wrong? The problem occurs because I'm trying to store the message string in a variable, and I'm trying to re-use the same variable at two places: the annotation's message attribute, and in the validation test for the model class. Should I not be checking for the presence of the message in my validation tests, but be checking for something else instead?
For anyone who comes across this later on, the research for this issue has led me to believe that comparing message strings in tests is not the way to go about writing validation test cases. Another downside to this approach is that you might have different validation messages for different locales. In that case, the message string itself might be a template e.g. my.message.key with its values in a ResourceBundle provided to Hibernate, i.e. files such as ValidationMessages.properties and ValidationMessages_de.properties.
In such a scenario, you could compare message for one locale in your validation test case, however, a better approach might be to check the annotation and the field for which the validation has failed. We can get both of these pieces of information via the ConstraintViolation and subsequently the ConstraintDescriptor types, provided by Hibernate. This way we can circumvent checking the message itself, but rely on the actual validation annotation which has failed.
As for the solution to this question, it seems it was a build cache issue. Cleaning maven's build cache results in this code working perfectly fine, but VSCode still seems to have an issue. For now, I will choose to ignore that.

Spring AMQP - Jackson2JsonMessageConverter setup _TypeId_ with interface instead of concrete Type since version 2.1

The problem
We have upgraded our Spring-boot version from 2.0.5 to 2.1.8.
As a result, Spring AMQP upgraded from 2.0.6 to 2.1.8 also.
Since then Jackson2JsonMessageConverter is unable to parse answer messages coming from methods annotated with #RabbitListener because they return an interface (actually declared as a generic in the code). And this interface is used to set the message _TypeId_ property .
with version 2.0 it used to set TypeId with the actual concrete class.
I did some digging and here is my understanding of the problem (code will follow)
When the method annotated with #RabbitListener returns, the MessagingMessageListenerAdapter#onMessage is invoked and encapsulates the result in a InvocationResult Object, which contains a genericType property.
This genericType is set up from the return type of the method annotated with #RabbitListener.
Then, it is used by the Jackson2JsonMessageConverter#createMessage method to setup _TypeId_ property.
On the other side, the Jackson2JsonMessageConverter#fromMessage can then parse the Json using this propery to find out the actual Type.
The problem is that, since the introduction of InvocationResult and genericType, our method annotated with #RabbitListener is declared as returning an interface and so the _TypeId_ property is set up with the interface instead of the actual concrete class. Here is the bit of code from Jackson2JsonMessageConverter#fromMessage (actualy from AbstractJackson2MessageConverter)which has changed (among other):
if (getClassMapper() == null) {
getJavaTypeMapper().fromJavaType(this.objectMapper.constructType(
genericType == null ? objectToConvert.getClass() : genericType), messageProperties);
}
else {
getClassMapper().fromClass(objectToConvert.getClass(), messageProperties); // NOSONAR never null
}
Since genericType is not null and contains the interfaceType... you can see our trouble.
Prior to version 2.1, we add no problem since the Jackson2JsonMessageConverter#createMessage() was always directly using objectToConvert.getClass():
if (getClassMapper() == null) {
getJavaTypeMapper().fromJavaType(this.jsonObjectMapper.constructType(objectToConvert.getClass()),
messageProperties);
}
else {
getClassMapper().fromClass(objectToConvert.getClass(),
messageProperties);
}
The code
Here is our code:
public abstract class AWorker<I extends RequestDtoInterface, O extends ResponseDtoInterface> {
#RabbitListener(queues = "${rabbit.worker.queue}"
, errorHandler = "workerErrorHandler"
, returnExceptions = "true")
public O receiveMessage(I inputMessage, #Header(LoggerUtil.LOGGER_MDC_ID) String mdcId, #Header(RabbitConstants.CONTEXT_INFO_HEADER_KEY) String contextInfoStr) {
if(inputMessage instanceof RequestDtoFirstImplementation.class){
return new ResponseDtoFirstImplementation();
}else{
return new ResponseDtoSecondImplementation();
}
}
}
Of course, the content of receiveMessage method is simplified, the point is that the actual implementation can return differents concretes types depending of the input concrete type
Possible solution
I figured two possibles workarounds, but none is really nice or easy to maintain.
The first would be to use a RemoteInvocationAwareMessageConverterAdapter to encapsulate the Jackson2jsonMessageConverter.
If I do that, the MessageConverter#toMessaget(Object object, MessageProperties messageProperties) is called instead of MessageConverter#toMessage(Object object, MessageProperties messageProperties, #Nullable Type genericType) and so we fall back to the old way.
But that sounds more like a hack than a proper solution. And if RemoteInvocationAwareMessageConverter changes its behaviour, we are back to the initial problem.
The second would be to use an existing ClassMapper but, even if it works, that sounds more difficult to maintain (especially when it comes to trustedPackages if we have a lot of class to serialize coming from different packages). Or implement a customClassMapper which also add a bit of work to do.
Since it used to work fine prior to version 2.1 I'm not sure it's absolutely necessary to reimplement ClassMapper.
But I can't find any simple way to make genericType set up with the concrete type

How do I add a dynamic number of annotated parameters to a new method in Bytebuddy?

I'm attempting to create a number of classes at runtime using ByteBuddy. So far I've managed to create the classes, add methods and annotate them. It's a great tool and I've enjoyed using it so far.
But now I'm stuck. I have my class, with a method and there are n number of parameters (dynamically controlled by config). This works something like...
DynamicType.Builder<?> builder = new ByteBuddy().subclass(Object.class)
.name(newClassName)
.annotateType(AnnotationDescription.Builder.ofType(Controller.class).build());
// loop for all methods to create
for (final MethodDefinition methodDefinition : methodDefinitions) {
final List<TypeDefinition> parameters = new ArrayList<>();
for (final MethodParameterDefinition methodParamDef : methodDefinition.getMethodParameterDefinitions()) {
parameters.add( TypeDescription.Generic.Builder.rawType(methodParamDef.getType()).build() );
}
// define the method
builder = builder
.defineMethod(methodDefinition.getName(), outputValueObjectClass, Modifier.PUBLIC)
.withParameters(parameters)
.annotateMethod(AnnotationDescription.Builder.ofType(RequestMapping.class)
.defineEnumerationArray("method", RequestMethod.class, RequestMethod.valueOf(methodDefinition.getHttpMethod()))
.defineArray("path", methodDefinition.getUrl()).build())
.annotateMethod(AnnotationDescription.Builder.ofType(ResponseBody.class).build())
}
final DynamicType.Unloaded unloadedClass = builder.make();
But when I attempt to add an annotation to one of the parameters by using the following code...
for (final MethodParameterDefinition methodParamDef : methodDefinition.getMethodParameterDefinitions()) {
parameters.add( TypeDescription.Generic.Builder.rawType(methodParamDef.getType())
.annotate(AnnotationDescription.Builder.ofType(PathVariable.class).define("name", methodParamDef.getName()).build())
.build() );
}
...I get the following exception....
java.lang.IllegalStateException: Illegal type annotations return type class...
If I know the number of method parameters I can add these using code like...
builder = builder
.defineMethod(methodDefinition.getName(), outputValueObjectClass, Modifier.PUBLIC)
.withParameter(methodParameterClass).annotateParameter(AnnotationDescription.Builder.ofType(ModelAttribute.class).build())
.annotateMethod(AnnotationDescription.Builder.ofType(RequestMapping.class)
.defineEnumerationArray("method", RequestMethod.class, RequestMethod.valueOf(methodDefinition.getHttpMethod()))
.defineArray("path", methodDefinition.getUrl()).build())
.annotateMethod(AnnotationDescription.Builder.ofType(ResponseBody.class).build())
But this doesn't work for a dynamic approach.
Does anyone know how to add a dynamic number of parameters (with annotations) to a method?
Thanks in advance!
I just checked the code and the exception message is misleading due to a copy-paste error. The error message should tell you that you are annotating a type with an annotation that is not a type annotation.
Note the difference between the annotation and the annotateParameter in the code generation DSL. The former annotates the type, the other one the parameter. This might seem confusing as the syntax is ambigous:
void foo(#Bar List<?> qux) { ... }
can mean both in Java, if #Bar is a type annotation, it annotates the List type, if it is a parameter annotation, it annotates the parameter qux, if it is both, the type and the parameter will be annotated. On the byte code level, you can choose what element to annotate. But since your annotation is only compatible to parameters, the (misleading, this is now fixed on master) error message is shown.
If you want to add a dynamic number of parameters, just run the loop on the DynamicType.Builder and add the parameters like this:
MethodDefinition.ParameterDefinition builder = ...
for (...) {
builder = builder.withParameter(...).annotateParameter(...);
}
This should do the trick.

org.dbunit.dataset.NoSuchColumnException

I'm getting the following error when I run my tests:
org.dbunit.dataset.NoSuchColumnException: myTable.MYFIELD - (Non-uppercase input column: myfield) in ColumnNameToIndexes cache map. Note that the map's column names are NOT case sensitive.
at org.dbunit.dataset.AbstractTableMetaData.getColumnIndex(AbstractTableMetaData.java:117)
I set a breakpoint in org.dbunit.dataset.AbstractTableMetaData#getColumnIndex and discovered the following. In IntelliJ Idea the method looks like this:
public int getColumnIndex(String columnName) throws DataSetException
{
logger.debug("getColumnIndex(columnName={}) - start", columnName);
if(this._columnsToIndexes == null)
{
// lazily create the map
this._columnsToIndexes = createColumnIndexesMap(this.getColumns());
}
String columnNameUpperCase = columnName.toUpperCase();
Integer colIndex = (Integer) this._columnsToIndexes.get(columnNameUpperCase);
if(colIndex != null)
{
return colIndex.intValue();
}
else
{
throw new NoSuchColumnException(this.getTableName(), columnNameUpperCase,
" (Non-uppercase input column: "+columnName+") in ColumnNameToIndexes cache map. " +
"Note that the map's column names are NOT case sensitive.");
}
}
The value of this.getColumns() does not contain any Column with Column.columnName matching the parameter columnName. Therefore colIndex becomes null and the exception is thrown.
It looks like DBUnit is looking for the column index in the wrong table meta data.
How can I fix this?
Note: I inherited this code from someone else (didn't write it).
I'm sensitive to the fact that you can't really share code. That does make things a little difficult, but here's an answer I think is reasonable given the confines:
I was able to easily reproduce this exception using a minimal Spring Boot/DbUnit project cloned from GitHub. Perhaps my observations will amount to the hint you're looking for, or at least inspire a better answer.
Steps
Clone the project and install dependencies.
Run the HsqldbexampleApplicationTests.contextLoads() test. It passes.
Get into StaticResource.java, and change one of the #Column annotations.
For example, I changed:
#Column(name = "CONTENT")
private String content;
to:
#Column(name = "CONTENTZ")
private String content;
Run the test again and observe the exception
Alternatively, you can get into sampleData.xml and change the CONTENT attributes there (the attribute name), to produce the same exception.
Observations
The test gets its data from /META-INF/dbtest/sampleData.xml. Note the CONTENT attribute.
The resource has an #Column annotation whose name must match an attribute found in the sampleData.xml elements.
Since your trouble is also with running tests, it may be that your code and the .xml(?) that hydrates your test data store are simply out of sync with respect to a column name.
Further implication of an XML file?
My attempts to provoke this exception by changing queries and instance variable names were unsuccessful. Everything I tried made the compiler complain, so I ruled it out.
For example, I also checked out this repo, and tried to change a query and an instance variable, but was thwarted by the compiler at every step. Changing a query:
Changing an instance variable name:
Where to look
Anywhere in any java code where you have #Column with MYFIELD inside it. Remember, annotations can span several lines in a file.
Any xml files containing MYFIELD.
Assuming the code under test works fine, and your problems are confined to running tests, the mechanism that injects data into your test is the prime suspect. If this isn't an xml file, what is it?
It's not clear from you post how do you get the _columnsToIndexes
It looks like a piece of some reflection code that depends on your POJO.
In this case the problem migth be in Lazy initialization of the object. Lazy initialized objects are not just entity object but some kind of proxy and attemption of getting its properties through the reflection may cause this problem.
Probably you should try add some kind of unproxy method into you createColumnIndexesMap. Here is example:
public static <T> T initializeAndUnproxy(T entity) {
if (entity == null) {
throw new InternalServerException("Entity passed for initialization is null");
}
T unproxy = entity;
Hibernate.initialize(entity);
if (isProxy(entity)) {
unproxy = (T) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
}
return unproxy;
}
public static <T> boolean isProxy(T entity) {
return entity instanceof HibernateProxy;
}
of course it depends on your ORM, here is example for Hibernate

URLClassLoader loads Annotation as com.sun.$Proxy$27

I'm trying to dynamically load a java class. The basic idea is, that a jar contains modules which get loaded dynamically at runtime. This is how I do it (I know it's hacky, but there is no other method to dynamically add a jar to an already existing classloader afaik):
Method method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class });
method.setAccessible(true);
method.invoke(moduleLoader, new Object[] { file.toURI().toURL() });
Class fooClass = moduleLoader.loadClass("com.coderunner.Foo");
Object foo = fooClass.newInstance();
Every module is annotated with an #Module annotation. So in order to gain further informations about the module, I try to get the annotation. The problem is that the annotation on foo is of type com.sun.$Proxy$27 instead of com.coderunner.Module and therefore I get a
ClassCastException: Cannot cast com.sun.proxy.$Proxy42 (id=64) to com.coderunner.Module
I have to say I'm a bit confused what happens here. Is what I want to do possible? How?
Edit: I maybe also should mention I'm trying this in a spring/spring-mvc and tomcat environment.
The fact that the reflection returns a proxy object does not prevent you from gathering information about the annotation and its values.
The getclass method returns a proxy object:
log.info("annotation class:" + annotation.getClass());
Output:
[INFO] annotation class:class com.sun.proxy.$Proxy16class
The output is that same as in your example, but that is no problem. Having the method (or field) is enough. The additional part is to just invoke the annotation method.
public void analyseClass(Class myClass) {
for (Method method: myClass.getMethods()) {
System.out.println("aanotations :" + Arrays.toString(field.getAnnotations()));
for (Annotation annotation : method.getAnnotations()) {
log.info("annotation class:" + annotation.getClass());
log.info("annotation class type:" + annotation.annotationType());
Class<Annotation> type = (Class<Annotation>) annotation.annotationType();
/* extract info only for a certain annotation */
if(type.getName().equals(MyAnnotation.class.getName())) {
String annotationValue =
(String) type.getMethod("MY_ANNOTATION_CERTAIN_METHOD_NAME").invoke(annotation);
log.info("annotationValue :" + annotationValue);
break;
}
}
}
//do the same for the fields of the class
for (Field field : myClass.getFields()) {
//...
}
}
To come to this solution, I used the following post:
How to get annotation class name, attribute values using reflection
The fact that you get a proxy in front of your annotation type should not matter. It might actually mislead you into believing that this is the cause for problems you are having. If stuff like "isAnnotationPresent(..)" fails, it is not due to that proxy, it is because you have loaded the annotation class multiple times using multiple classloaders. For example, Jetty gives priority to the WebApp classloader by default. So if your Jetty server instance (or Tomcat or whatever) already has loaded the annotation class, and the annotation is on your WebApp's classpath, too, you can have problems like "getAnnotation()" not returning anything. Just make sure that the library containing your annotation is not loaded twice.
The solution provided by Andreas is, well, a very dirty workaround and just covers up the fact that you probably don't have your classloading under control/properly organized.
I had the same problem when trying to create a ant task for code generation based on a declarative approach using annotations.
I found that the documentation of the Proxy - Object states that instanceof should resolve it,
but this didn't work fopr me neither.
I finally got a around with
Annotation[] annotations = classObj.getAnnotations();
for(int i = 0;i < annotations.length;i++) {
Class<?>[] interfaces = annotations[i].getClass().getInterfaces();
for(int i2 = 0; i2 < interfaces.length;i2++) {
System.out.println("interface:" + interfaces[i2].getName());
}
giving my the name of the original annotation, so comparing this name to the annotations classname will give you the desired result.

Categories