4

Given a POJO in Spring Boot with several dozen fields of type String which is deserialized by Jackson. For demonstration purposes the following example only contains three fields:

@NoArgsConstructor
public class SomeRequest {

  @JsonProperty("field_1")
  private String field1;

  @JsonProperty("field_2")
  private String field2;

  @JsonProperty("field_3")
  private String field3;
}

I'm looking for a way to override the setter method but only for certain fields, i.e. I'd like to avoid repeating the below code for every affected field. This is doable for a handful number of fields but gets tedious for more than a handful.

public setField2(String field2) {
  this.field2 = field2 + "?";
}

My idea was to place an annotation on the field like this:

@NoArgsConstructor
public class SomeRequest {

  // ...

  @JsonProperty("field_2")
  @AppendQuestionMark
  private String field2;

  // ...
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AppendQuestionMark {
}

But I'm lacking information on how to "implement" the AppendQuestionMark annotation which would override the field's setter method.

Or am I thinking way too complicated?

Robert Strauch
  • 12,055
  • 24
  • 120
  • 192
  • Would it also serve the purposes of your use case if this was done in the getter method? – João Dias Sep 26 '21 at 00:05
  • Yes, that would also serve the purpose if it makes things easier – Robert Strauch Sep 26 '21 at 08:08
  • 1
    My idea when I saw the problem was create a custom deserializer and use a `@JsonDeserialize` annotation over the fields you need instead of a custom annotation , could you use them in you project ? – dariosicily Sep 26 '21 at 09:23

3 Answers3

3

You can't change the settermethod's body if that's what you are asking. But you can create a method that will take an object (i.e. SomeRequest) as input and check which fields have your Annotation and change the values for those fields as you want.

For example, I created an annotation AppendStr.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AppendStr {
    public String str();;
}

Then I created another class 'AppendStrImpl` that will handle the implementation. I used the following code -

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class AppendStrImpl {
    public void changeFields(Object object) throws Exception {
        Class<?> clazz = object.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(AppendStr.class)) {
                // get the getter method name from the field name
                String fieldName = field.getName();
                String getterMethodName =
                        "get" +
                        fieldName.substring(0, 1).toUpperCase() +
                        fieldName.substring(1);
                Method getterMethod = clazz.getMethod(getterMethodName);
                String returnValue = (String) getterMethod.invoke(object);

                String setterMethodName = getterMethodName.substring(0, 1).replace("g", "s")
                        + getterMethodName.substring(1);
                Method setterMethod = clazz.getMethod(setterMethodName, String.class);
                setterMethod.invoke(object, returnValue + getAppendingString(field));
                System.out.println((String) getterMethod.invoke(object));
            }
        }
    }

    private String getAppendingString(Field field) {
        return field.getAnnotation(AppendStr.class)
                .str();
    }
}

And this is my POJO class -

public class POJO {
    @AppendStr(str = "?")
    private String filed1;

    @AppendStr(str = "!")
    private String filed2;

    private String filed3;

    @AppendStr(str = "+")
    private String filed4;

    // ... getters and setters
}

Then I called this method from the main method -

POJO pojo = new POJO("a", "b", "c", "d");
AppendStrImpl appendStrImpl = new AppendStrImpl();
try {
    appendStrImpl.changeFields(pojo);
} catch (Exception e) {
    e.printStackTrace();
}

Now you can make this call with hard coding or you can use @Aspect too if you want.

The github link is here.

omar jayed
  • 850
  • 5
  • 16
1

Instead of creating a new annotation that appends a question mark to one generic string field in your pojo you can use the already present JsonDeserialize annotation over the string fields you are interested:

@Data
@NoArgsConstructor
public class SomeRequest {

  @JsonProperty("field_1")
  private String field1;

  @JsonProperty("field_2")
  //here the custom deserializer appends the question mark character
  @JsonDeserialize(using = StringAppendQuestionMarkDeserializer.class)
  private String field2; 
  
}

In your spring boot project you can register the custom deserializer with the JsonComponent annotation like below:

@JsonComponent
public class StringAppendQuestionMarkDeserializer extends JsonDeserializer<String> {

    @Override
    public String deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        return node.asText() + "?";
    }

}

A spring boot test example using the custom deserializer:

@JsonTest
class CorespringApplicationTests {

    @Test
    void testDeserialize() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        SomeRequest request = mapper.readValue("{\"field_1\":\"value1\",\"field_2\":\"value2\"}", SomeRequest.class);
        System.out.println(request); //<-- SomeRequest(field1=value1, field2=value2?)

    }

}
dariosicily
  • 4,239
  • 2
  • 11
  • 17
0

Something like the following should do the trick:

@Aspect
@Component
public class AppendQuestionMarkAspect {

    @Around("@annotation(AppendQuestionMark)")
    public Object appendQuestionMark(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] arguments = joinPoint.getArgs();
        return joinPoint.proceed(new Object[] {((String) arguments[0]) + "?"});
    }

}

Of course, it would be advisable to check that only one argument exists and that it is, in fact, a String. Or you can also define the pointcut as to be applied only to methods starting with set. But the essence of the code is there.

João Dias
  • 16,277
  • 6
  • 33
  • 45
  • 1
    I am also trying this but seems Pointcut is to getting invoked, https://stackoverflow.com/questions/69409931/around-pointcut-not-getting-invoked-for-custom-annotation – Vikas Oct 01 '21 at 17:42
  • This is not a correct answer. The class in which annotation is located is not defined Spring Context. That's why processes inside `@Around` method will not invoked. – fatih Jun 03 '22 at 12:38