在 Spring 引导应用程序中序列化 API 请求参数的问题

Issues in serializing API request argument in Spring boot application

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

  @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 作为请求参数,上述代码片段将失败。

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

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 的请求类型,我收到 Whosebug 错误。

其实,我想避免这些类型的争论。但是我想不出任何正确处理这个问题的方法。

有人可以帮忙吗?

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

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

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

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

    1. 只需在您的方面或
    2. 中保留 classes 的静态排除列表
    3. 构建一个动态列表,使用 try/catch 块并添加 class 杰克逊无法序列化到列表中的元素,下次避免序列化相同的 class,或
    4. 总是使用 try/catch,回到 toString()

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

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

  • 例如,由于 void 方法中的 return mapper.writeValueAsString(..) 语句,它永远不会像这样编译。
  • 您绑定 returnValue 但从未使用它。
  • 您在三个不同的地方调用 codeSignature.getParameterNames(),其中一个在循环中,而不是将值缓存在局部变量中。这应该被简化。
  • 您可以将签名转换为 MethodSignature 而不是更通用的 CodeSignature。然后您将有权访问该方法的 return 类型。 Spring AOP 无论如何都不支持拦截构造函数,只有AspectJ 支持。假设你使用Spring AOP,你唯一能拦截的就是方法。
  • 我不明白为什么你在每个方法参数值上调用 mapper.convertValue(..),试图将它转换成 Map。为什么不直接使用 writeValueAsString(..) 呢?
  • 您检查 getParameterNames() 是否有 null,但它从来没有 return 和 null,而是一个空数组。所以这个检查是没有必要的。
  • 另请注意,只有在使用调试信息编译 class 时,您存储参数名称的整个想法才有效。否则就不会有任何真正的参数名称,只有像 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));
  }
}

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

控制台日志:

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