从不同类型的方法参数中提取特定字段的值

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

我正在为我的 spring 引导应用程序中的服务方法编写一个拦截器,如下所示:

@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 {
   ...
    
  }
}

其中一项服务如下:

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

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

另一种服务可以如下:

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

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

这里我的目标是从关联的方法参数中提取特定字段的值。为了做到这一点,我可以在方面编写一个函数,其中我可以有多个 if-else 块,如下所示:

 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) {
       ...
     }
     ...
    }

从上面的代码片段可以看出,可以有以下几种情况:

  1. 方法参数可以是对象,也可以是原始类型。
  2. 可以有零个或多个方法参数。

此外,我也无法更改 DTO 对象。

我想知道是否有更好的方法来做到这一点。基本上,我正在寻找一种使其易于扩展的方法。

有人可以帮忙吗?谢谢。

编辑

到目前为止,为了处理方法参数,我首先收集了如下方法参数:

      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");
  }

实际上,这里的问题是我要查找的字段在 DTO 中的名称不同。例如,在 ADTO 中,该字段被命名为 id,而在 BDTO 中,它被命名为 aId.

如何使您的 ADTO/BDTO 实现自定义界面

interface AspectDataProvider {
  String aspectSpecificData()
}

然后在你的参数处理器中,你可以轻松做到

if (arg instanceof AspectDataProvider) {
  return ((AspectDataProvider) arg).aspectSpecificData();
}

或者您可以更深入地挖掘并为您的实施加入访问者模式

  interface AspectImplementor {
     void executeAspect(String value);
  }

  interface AspectAware {
     void accept(AspectImplementor aspect);
  }

然后在您的 BDTO ADTO 中添加 AspectAware

的自定义实现
  void accept(AspectImplementor aspect) {
    String property = // object-specific property extraction
    aspect.executeAspect(property);
  }

而在你这方面,可以写类似

  if (arg instanceof AspectAware ) {
    ((AspectAware ) arg).accept(aspectExecutorInstance);
  }

但我不建议从一开始就使用设计模式 - 一旦您确定您拥有访问者模式,它应该始终是下一个改进。

好的,那我建议如下:

我假设,您不能更改 DTO 对象 我假设,有时你的方法甚至没有 DTO 参数,但如果它们存在,你有一个逻辑来“找到”它们

如果知道拦截的DTO是什么类型,就可以创建接口:

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

为每个 DTO 实现此接口

例如,如果您计划拦截 2 种不同类型的 DTO:

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

然后执行:

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

现在将所有这些处理程序定义为 spring bean,并将它们的列表注入方面。 Spring boot 会将它们全部作为列表注入,顺序无关紧要

在方面的构造函数中创建 Class -> DTOHandler 的映射。 迭代抛出列表并创建映射的键作为 'getDTOClass' 的调用,并将值作为处理程序本身:

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);
      }
   }
}

使用此设置,extractMyField 将如下所示:

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
     }
}

更新(基于 Op 的评论):

为了真正找到具有 'myfield' 值的参数,您使用了相当复杂的逻辑(在问题的编辑部分)。

相反,您可以创建可应用于参数的注释(运行时保留)。

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

然后您可以使用此注释对服务中的参数进行注释,以“帮助”您的方面了解哪些参数实际上是 DTO:

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

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

那么切面的解析逻辑会变成这样:

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