Search code examples
javaspringspring-bootaopaspectj

Extract the value of a particular field from different types of method arguments


I am writing an interceptor for the service methods in my spring boot application like below:

@Aspect
public class MyAspect {

  public MyAspect() {
   
  }

  @Pointcut("@within(org.springframework.stereotype.Service)")
  public void applicationServicePointcut() {
  }

  @Before(value = ("applicationServicePointcut()"))
  public void process(JoinPoint joinPoint) throws Exception {
   ...
    
  }
}

One such service is as follows:

@Service
@Transactional
public class AService {
   
  public ADTO create(ADTO aDTO) {
    ...
  }

  public ADTO update(ADTO aDTO) {
    ...
  }
}

Another service can be as follows:

@Service
@Transactional
public class BService {
   
  public BDTO create(BDTO bDTO) {
    ...
  }

  public BDTO update(BDTO bDTO) {
    ...
  }
  
  public void doSomething(String a, int b) {
    ...
  }
}

Here my goal is to extract the value of a particular field from the associated method arguments. In order to do this, I can write a single function in aspect, where I can have multiple if-else blocks as follows:

 String extractMyField(JoinPoint joinPoint) {
  
     //extract the arguments of the associated method from joinpoint
     //depending on the type of arguments, extract the value of myfield as follows

    if(arg instance of ADTO) {
       ...
     } else if (arg instance of BDTO) {
       ...
     }
     ...
    }

It can be seen in the above code snippet, there can be the following cases:

  1. the method argument can be an object as well as primitive types as well.
  2. there can be zero or more method arguments.

Also, I cannot change the DTO objects as well.

I am wondering if there can be a better approach to do this. Basically, I am looking for a way to make it easily extensible.

Could anyone please help here? Thanks.

EDIT

As of now, in order to process the method arguments, first I have been collecting the method arguments as follows:

      Map<String, Object> getMethodArguments(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    
        Map<String, Object> argumentNameValueMap = new HashMap<>();
    
        if (methodSignature.getParameterNames() == null) {
          return argumentNameValueMap;
        }
    
        for (int i = 0; i < methodSignature.getParameterNames().length; i++) {
          String argumentName = methodSignature.getParameterNames()[i];
          Object argumentValue = joinPoint.getArgs()[i];
          Class argumentType = joinPoint.getArgs()[i].getClass();
          //check if i th parameter is json serializable
          if (Objects.isNull(methodSignature.getParameterTypes()) ||
              isJsonSerializable(methodSignature.getParameterTypes()[i])) {
            argumentNameValueMap.put(argumentName, mapper.convertValue(argumentValue, Map.class));
          }
        }
        return argumentNameValueMap;
       }

        private boolean isJsonSerializable(Class parameterType) {
    return
        !Objects.isNull(parameterType) &&
            !parameterType.getName().endsWith("HttpServletRequest") &&
            !parameterType.getName().endsWith("HttpServletResponse") &&
            !parameterType.getName().endsWith("MultipartFile");
  }

Actually, the catch here is that the field I am looking for is named differently across DTOs. For example, in ADTO the field is named as id whereas, in BDTO, it's named as aId.


Solution

  • Ok, then let me suggest the following:

    I assume, you can't change the DTO objects I assume, sometimes your methods won't even have DTO parameters, but you have a logic to "find" them if they exist

    If you know what are the types of the intercepted DTOs are, you can create the interface:

    interface DTOHandler {
       Class<?> getDTOClass();
       Object getMyField(Object dtoObject); 
    }
    

    Implement this interface for each DTO

    For example, if you plan to intercept 2 different types of DTOs:

    class ADTO {
      Integer myField;
    }
    class BDTO {
      String myField;
    }
    

    Then implement:

    class DTOAHandler implements DTOHandler {
       Class<?> getDtoClass() {return ADTO.class;}
       Object getMyField(Object obj) {return ((ADTO)obj).getMyField();}
    }
    
    // and do a similar implementation same for DTOB
    

    Now define all these handlers as spring beans and inject a list of them into the aspect. Spring boot will inject them all as a list, the order doesn't matter

    In the constructor of the aspect create a map of Class<?> -> DTOHandler. Iterate throw the list and create a key of the map as a call of 'getDTOClass' and the value as a handler itself:

    public class MyAspect {
    
       private Map<Class<?>, DTOHandler> handlersRegistry;
    
       public MyAspect(List<DTOHandler> allHandlers) {
          handlersRegistry = new HashMap<>();
          for(DTOHandler h : allHandlers) {
            handlersRegistry.put(h.getDTOClass(), h);
          }
       }
    }
    

    With this setup the extractMyField will look as follows:

    public void extractMyField(JoinPoint jp) {
        
         Object myDTO = findDTOParameter(jp);
         
         DTOHandler handler = this.handlersRegistry.get(myDTO.getClass());
         if(handler != null) {
           // there is an actual handler for this type of DTOs
            Object myField = handler.getMyField(myDTO);
         }
         else {
           // the aspect can't handle the DTO of the specified type
         }
    }
    

    Update (based on Op's comment):

    In order to actually find a parameter that has a 'myfield' value, you've used a pretty complicated logic (in the EDIT section of the question).

    Instead you can create an annotation (runtime retention) that can be applied to the parameters.

    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface @DTO {
    }
    

    Then you can annotate with this annotation the parameters in the service to "help" your aspect to understand which of the parameters are actually DTOs:

    @Service
    @Transactional
    public class AService {
       
      public ADTO create(@DTO ADTO aDTO) {
        ...
      }
    
      public ADTO update(@DTO ADTO aDTO) {
        ...
      }
    }
    

    Then, the resolution logic in the aspect will become something like:

    // pseudocode
    foreach p in 'parameters' {
      if(isAnnotatedWith(p, DTO.class))  {
         // it matches
      }
    }