【问题标题】:Issues in serializing API request argument in Spring boot application在 Spring Boot 应用程序中序列化 API 请求参数的问题
【发布时间】:2020-11-11 07:49:23
【问题描述】:

我已经编写了一个方面来序列化 Spring boot 应用程序中 API 的请求参数,在 DB 中如下:

  @Pointcut("within(com.tm.web.rest.*)")
  public void applicationResourcePointcut() {
    // Method is empty as this is just a Pointcut, the implementations are in the advices.
  }


  /**
   * Advice that logs when a method is returned.
   *
   * @param joinPoint join point for advice
   */
  @AfterReturning(value = ("applicationResourcePointcut()"),
      returning = "returnValue")


public void capturePayloadWhileReturning(JoinPoint joinPoint, Object returnValue)  {
    
    CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();

    Map<String, Object> argumentNameValueMap = new HashMap<>();

    if (codeSignature.getParameterNames() == null) {
      return mapper.writeValueAsString(argumentNameValueMap);
    }

    for (int i = 0; i < codeSignature.getParameterNames().length; i++) {
      String argumentName = codeSignature.getParameterNames()[i];
      Object argumentValue = joinPoint.getArgs()[i];
      argumentNameValueMap.put(argumentName, mapper.convertValue(argumentValue, Map.class));
    }
    String s = mapper.writeValueAsString(argumentNameValueMap); 
}

如果我们将HttpServletRequest/ByteStream 作为请求参数,上述代码 sn-p 将失败。

例如,对于字节流,我得到以下异常:

java.lang.IllegalArgumentException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile["inputStream"])
  

对于 HttpServletRequest 的请求类型,我收到 StackOverflow 错误。

实际上,我想避免这些类型的论点。但我无法找出任何正确处理此问题的方法。

有人可以帮忙吗?

【问题讨论】:

    标签: java spring-boot aspectj spring-aop objectmapper


    【解决方案1】:

    Joy,如果您提出问题,请尽量提供完整的MCVE,不要让想帮助您猜测的志愿者猜测。在这种情况下,您在序列化数据时遇到了问题,但您既没有提到您使用哪种序列化技术或工具,也没有从您的代码中识别出它,因为方面建议使用了一个对象 mapper,而您没有展示它是如何声明的。我不明白为什么这么多开发人员选择简洁而不是清晰。

    mapper.writeValueAsString(..) 上进行了一些谷歌搜索后,我发现您可能使用的是 Jackson。我会假设这是真的。

    1. 因此,解决问题的一种方法是为有问题的类编写自定义序列化程序,请参阅this tutorial。通过调整映射器配置也可以避免一些序列化异常。

    2. 另一种方法是完全避免序列化(或“json-ising”)这些对象,而是将一些虚拟值或toString() 的结果写入数据库,无论如何。这是你问的吗?那你可以

      1. 只需在您的方面保留一个静态的排除类列表或
      2. 构建一个动态列表,使用 try/catch 块并将 Jackson 无法序列化的类添加到列表中,下次避免对同一类进行序列化,或者
      3. 始终使用try/catch,回退到toString()

    我认为#1 总体上会更好,但因为你的问题是关于 AOP 而不是关于 Jackson(也根据你选择的标签),所以我将向你展示 #2.3。

    进一步查看您的示例代码,它看起来有点奇怪:

    • 例如,由于void 方法中的return mapper.writeValueAsString(..) 语句,它永远不会像这样编译。
    • 您绑定了returnValue,但从不使用它。
    • 您在三个不同的地方调用codeSignature.getParameterNames(),其中一个在循环中,而不是将值缓存在局部变量中。这应该被简化。
    • 您可以将签名转换为MethodSignature,而不是更通用的CodeSignature。然后您将可以访问该方法的返回类型。 Spring AOP 无论如何都不支持拦截构造函数,只有 AspectJ 支持。假设你使用 Spring AOP,你唯一可以拦截的就是方法。
    • 我不明白你为什么在每个方法参数值上调用mapper.convertValue(..),试图将其转换为Map。为什么不直接使用writeValueAsString(..)
    • 您检查getParameterNames() 是否有null,但它永远不会返回null,而是一个空数组。所以这个检查是没有必要的。
    • 还请注意,您存储参数名称的整个想法只有在使用调试信息编译类时才有效。否则不会有任何真正的参数名称,只有 arg0arg1 等代理名称。因此,在实施这样的解决方案之前,您宁愿非常确定代码是否以正确的方式编译。
    • 在已经包含 JSON 对象的地图上调用 mapper.writeValueAsString(argumentNameValueMap) 会导致像 "foo" 这样的字符串再次像 "\"foo\"" 这样被双引号括起来,这可能不是您想要的。确保每个对象只序列化一次。

    这是我的MCVE

    示例组件:

    package de.scrum_master.spring.q64782403;
    
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import java.io.ByteArrayInputStream;
    
    @Component
    public class MyComponent {
      public void doSomething() {
        System.out.println("Doing something");
      }
    
      public int add(int a, int b) {
        System.out.println("Adding");
        return a+b;
      }
    
      public void someRequest(HttpServletRequest request, String parameter) {
        System.out.println("Handling request");
      }
      public void someByteStream(int index, ByteArrayInputStream stream) {
        System.out.println("Handling byte array input stream");
      }
      public String concatenate(String a, String b) {
        System.out.println("Concatenating");
        return a + " " + b;
      }
    }
    

    驱动程序应用:

    package de.scrum_master.spring.q64782403;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.mock.web.MockHttpServletRequest;
    
    import java.io.ByteArrayInputStream;
    
    @SpringBootApplication
    public class Application {
      public static void main(String[] args) {
        try (ConfigurableApplicationContext context = SpringApplication.run(Application.class, args)) {
          doStuff(context);
        }
      }
    
      private static void doStuff(ConfigurableApplicationContext context) {
        MyComponent myComponent = context.getBean(MyComponent.class);
        myComponent.doSomething();
        myComponent.add(4, 5);
        myComponent.someByteStream(11, new ByteArrayInputStream(new byte[1024]));
        myComponent.someRequest(new MockHttpServletRequest("GET", "/my/request"), "foo");
        myComponent.concatenate("Hello", "world");
      }
    }
    

    请注意,对于这个虚拟应用程序,我只使用MockHttpServletRequest,所以如果你想编译它,你需要添加org.springframework:spring-test 作为编译依赖项。

    方面:

    package de.scrum_master.spring.q64782403;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    @Aspect
    public class SerialiserAspect {
      ObjectMapper mapper = new ObjectMapper();
    
      @AfterReturning(
        value = "within(de.scrum_master.spring.q64782403..*)",
        returning = "returnValue"
      )
      public void capturePayloadWhileReturning(JoinPoint joinPoint, Object returnValue)
        throws JsonProcessingException
      {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] argumentNames = signature.getParameterNames();
        Object[] argumentValues = joinPoint.getArgs();
        assert argumentNames.length == argumentValues.length;
    
        System.out.println(joinPoint);
        System.out.println("  Argument names  = " + Arrays.deepToString(argumentNames));
        System.out.println("  Argument types  = " + Arrays.deepToString(signature.getParameterTypes()));
        System.out.println("  Argument values = " + Arrays.deepToString(argumentValues));
        System.out.println("  Return type     = " + signature.getReturnType());
        System.out.println("  Return value    = " + returnValue);
    
        Map<String, Object> arguments = new HashMap<>();
        for (int i = 0; i < argumentNames.length; i++) {
          String argumentName = argumentNames[i];
          Object argumentValue = argumentValues[i];
          try {
            mapper.writeValueAsString(argumentValue);
          }
          catch (JsonProcessingException e) {
            argumentValue = argumentValue.toString();
            System.out.println("Serialisation problem, falling back to toString():\n  " + e);
          }
          arguments.put(argumentName, argumentValue);
        }
        System.out.println(mapper.writeValueAsString(arguments));
      }
    }
    

    将连接点、参数和返回值记录到控制台的第一块只是为了帮助您了解方面在做什么。

    控制台日志:

    2020-11-12 10:04:39.522  INFO 19704 --- [           main] d.s.spring.q64782403.Application         : Started Application in 4.49 seconds (JVM running for 6.085)
    Doing something
    execution(void de.scrum_master.spring.q64782403.MyComponent.doSomething())
      Argument names  = []
      Argument types  = []
      Argument values = []
      Return type     = void
      Return value    = null
    {}
    Adding
    execution(int de.scrum_master.spring.q64782403.MyComponent.add(int,int))
      Argument names  = [a, b]
      Argument types  = [int, int]
      Argument values = [4, 5]
      Return type     = int
      Return value    = 9
    {"a":4,"b":5}
    Handling byte array input stream
    execution(void de.scrum_master.spring.q64782403.MyComponent.someByteStream(int,ByteArrayInputStream))
      Argument names  = [index, stream]
      Argument types  = [int, class java.io.ByteArrayInputStream]
      Argument values = [11, java.io.ByteArrayInputStream@1e3ff233]
      Return type     = void
      Return value    = null
    Serialisation problem, falling back to toString():
      com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
    {"stream":"java.io.ByteArrayInputStream@1e3ff233","index":11}
    Handling request
    execution(void de.scrum_master.spring.q64782403.MyComponent.someRequest(HttpServletRequest,String))
      Argument names  = [request, parameter]
      Argument types  = [interface javax.servlet.http.HttpServletRequest, class java.lang.String]
      Argument values = [org.springframework.mock.web.MockHttpServletRequest@9accff0, foo]
      Return type     = void
      Return value    = null
    Serialisation problem, falling back to toString():
      com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.util.Collections$3 and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.mock.web.MockHttpServletRequest["servletContext"]->org.springframework.mock.web.MockServletContext["servletNames"])
    {"request":"org.springframework.mock.web.MockHttpServletRequest@9accff0","parameter":"foo"}
    Concatenating
    execution(String de.scrum_master.spring.q64782403.MyComponent.concatenate(String,String))
      Argument names  = [a, b]
      Argument types  = [class java.lang.String, class java.lang.String]
      Argument values = [Hello, world]
      Return type     = class java.lang.String
      Return value    = Hello world
    {"a":"Hello","b":"world"}
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-12-04
      • 2021-01-07
      • 2022-10-19
      • 2019-07-25
      • 2018-02-05
      • 2020-10-19
      • 2020-07-13
      • 1970-01-01
      相关资源
      最近更新 更多